# What this PR does

## Which issue(s) this PR fixes

## Checklist

- [ ] Tests updated
- [ ] Documentation added
- [x] `CHANGELOG.md` updated
This commit is contained in:
Maxim Mordasov 2023-02-27 19:27:11 +03:00 committed by GitHub
parent c0873007b0
commit bee9943706
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 336 additions and 269 deletions

View file

@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed UI permission related bug where Editors could not export their user iCal link
- Fixed error when a shift is created using Etc/UTC as timezone
- Fixed issue with refresh ical file task not considering empty string values
- Schedules: Long popup does not fit screen & buttons unreachable & objects outside of the popup ([1002](https://github.com/grafana/oncall/issues/1002))
- Can't scroll on integration settings page ([415](https://github.com/grafana/oncall/issues/415))
- Team change in the Integration page always causes 403 ([1292](https://github.com/grafana/oncall/issues/1292))
- Schedules: Permalink doesn't work with multi-teams ([940](https://github.com/grafana/oncall/issues/940))
- Schedules list -> expanded schedule blows page width ([1293](https://github.com/grafana/oncall/issues/1293))
### Changed

View file

@ -7,7 +7,7 @@
margin-left: auto;
margin-right: auto;
top: 10%;
max-height: 80%;
max-height: 90%;
display: flex;
flex-direction: column;
border-image: initial;
@ -18,6 +18,7 @@
box-shadow: var(--shadows-z3);
border-radius: 2px;
z-index: 10;
overflow: scroll;
}
/*

View file

@ -48,6 +48,8 @@
display: flex;
flex-direction: column;
gap: 1px;
max-height: 300px;
overflow: scroll;
}
.user {
@ -55,7 +57,6 @@
border-radius: 2px;
display: flex;
position: relative;
overflow: hidden;
}
.user-buttons {

View file

@ -2,6 +2,15 @@
display: block;
}
.title {
background: var(--background-primary);
top: -15px;
position: sticky;
margin: -15px -15px 0 -15px;
padding: 15px;
z-index: 10;
}
.draggable {
top: 0;

View file

@ -277,151 +277,155 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
</Draggable>
)}
>
<VerticalGroup>
<HorizontalGroup justify="space-between">
<Text size="medium">
<HorizontalGroup spacing="sm">
<span>[L{shiftId === 'new' ? layerPriority : shift?.priority_level}]</span>
{shiftId === 'new' ? 'New Rotation' : 'Update Rotation'}
</HorizontalGroup>
</Text>
<HorizontalGroup>
{shiftId !== 'new' && (
<IconButton
variant="secondary"
tooltip="Delete"
name="trash-alt"
onClick={() => setShowDeleteRotationConfirmation(true)}
/>
)}
<IconButton variant="secondary" className={cx('drag-handler')} name="draggabledots" />
</HorizontalGroup>
</HorizontalGroup>
<div className={cx('content')}>
<VerticalGroup>
<div className={cx('two-fields')}>
<Field
label={
<Text type="primary" size="small">
Rotation start
</Text>
}
>
<DateTimePicker
minMoment={shiftStart}
value={rotationStart}
onChange={setRotationStart}
timezone={currentTimezone}
onFocus={getFocusHandler('rotationStart')}
onBlur={handleBlur}
/>
</Field>
<Field
label={
<HorizontalGroup spacing="xs">
<Text type="primary" size="small">
Rotation end
</Text>
<InlineSwitch
className={cx('inline-switch')}
transparent
value={!endLess}
onChange={handleChangeEndless}
/>
</HorizontalGroup>
}
>
{endLess ? (
<div style={{ lineHeight: '32px' }}>
<Text type="secondary">Endless</Text>
</div>
) : (
<DateTimePicker value={rotationEnd} onChange={setRotationEnd} timezone={currentTimezone} />
)}
</Field>
</div>
<>
<div className={cx('title')}>
<HorizontalGroup justify="space-between">
<Text size="medium">
<HorizontalGroup spacing="sm">
<span>[L{shiftId === 'new' ? layerPriority : shift?.priority_level}]</span>
{shiftId === 'new' ? 'New Rotation' : 'Update Rotation'}
</HorizontalGroup>
</Text>
<HorizontalGroup>
<Field className={cx('control')} label="Repeat shifts every">
<Select
maxMenuHeight={120}
value={repeatEveryValue}
options={repeatShiftsEveryOptions}
onChange={handleRepeatEveryValueChange}
allowCustomValue
{shiftId !== 'new' && (
<IconButton
variant="secondary"
tooltip="Delete"
name="trash-alt"
onClick={() => setShowDeleteRotationConfirmation(true)}
/>
</Field>
<Field className={cx('control')} label="">
<RemoteSelect
href="/oncall_shifts/frequency_options/"
value={repeatEveryPeriod}
onChange={setRepeatEveryPeriod}
/>
</Field>
)}
<IconButton variant="secondary" className={cx('drag-handler')} name="draggabledots" />
</HorizontalGroup>
{(repeatEveryPeriod === 0 || repeatEveryPeriod === 1) && (
<Field label="Select days to repeat">
<DaysSelector
options={store.scheduleStore.byDayOptions}
value={selectedDays}
onChange={(value) => setSelectedDays(value)}
/>
</Field>
)}
<div className={cx('two-fields')}>
<Field
className={cx('date-time-picker')}
label={
<Text type="primary" size="small">
Parent shift start
</Text>
}
>
<DateTimePicker
value={shiftStart}
onChange={updateShiftStart}
timezone={currentTimezone}
onFocus={getFocusHandler('shiftStart')}
onBlur={handleBlur}
/>
</Field>
<Field
className={cx('date-time-picker')}
label={
<Text type="primary" size="small">
Parent shift end
</Text>
}
>
<DateTimePicker
value={shiftEnd}
onChange={setShiftEnd}
timezone={currentTimezone}
onFocus={getFocusHandler('shiftEnd')}
onBlur={handleBlur}
/>
</Field>
</div>
<UserGroups
value={userGroups}
onChange={setUserGroups}
isMultipleGroups={true}
renderUser={renderUser}
showError={!isFormValid}
/>
</VerticalGroup>
</div>
<HorizontalGroup justify="space-between">
<Text type="secondary">Timezone: {getTzOffsetString(dayjs().tz(currentTimezone))}</Text>
<HorizontalGroup>
<Button variant="secondary" onClick={onHide}>
{shiftId === 'new' ? 'Cancel' : 'Close'}
</Button>
<Button variant="primary" onClick={handleCreate} disabled={!isFormValid}>
{shiftId === 'new' ? 'Create' : 'Update'}
</Button>
</HorizontalGroup>
</HorizontalGroup>
</VerticalGroup>
</div>
<VerticalGroup>
<div className={cx('content')}>
<VerticalGroup>
<div className={cx('two-fields')}>
<Field
label={
<Text type="primary" size="small">
Rotation start
</Text>
}
>
<DateTimePicker
minMoment={shiftStart}
value={rotationStart}
onChange={setRotationStart}
timezone={currentTimezone}
onFocus={getFocusHandler('rotationStart')}
onBlur={handleBlur}
/>
</Field>
<Field
label={
<HorizontalGroup spacing="xs">
<Text type="primary" size="small">
Rotation end
</Text>
<InlineSwitch
className={cx('inline-switch')}
transparent
value={!endLess}
onChange={handleChangeEndless}
/>
</HorizontalGroup>
}
>
{endLess ? (
<div style={{ lineHeight: '32px' }}>
<Text type="secondary">Endless</Text>
</div>
) : (
<DateTimePicker value={rotationEnd} onChange={setRotationEnd} timezone={currentTimezone} />
)}
</Field>
</div>
<HorizontalGroup>
<Field className={cx('control')} label="Repeat shifts every">
<Select
maxMenuHeight={120}
value={repeatEveryValue}
options={repeatShiftsEveryOptions}
onChange={handleRepeatEveryValueChange}
allowCustomValue
/>
</Field>
<Field className={cx('control')} label="">
<RemoteSelect
href="/oncall_shifts/frequency_options/"
value={repeatEveryPeriod}
onChange={setRepeatEveryPeriod}
/>
</Field>
</HorizontalGroup>
{(repeatEveryPeriod === 0 || repeatEveryPeriod === 1) && (
<Field label="Select days to repeat">
<DaysSelector
options={store.scheduleStore.byDayOptions}
value={selectedDays}
onChange={(value) => setSelectedDays(value)}
/>
</Field>
)}
<div className={cx('two-fields')}>
<Field
className={cx('date-time-picker')}
label={
<Text type="primary" size="small">
Parent shift start
</Text>
}
>
<DateTimePicker
value={shiftStart}
onChange={updateShiftStart}
timezone={currentTimezone}
onFocus={getFocusHandler('shiftStart')}
onBlur={handleBlur}
/>
</Field>
<Field
className={cx('date-time-picker')}
label={
<Text type="primary" size="small">
Parent shift end
</Text>
}
>
<DateTimePicker
value={shiftEnd}
onChange={setShiftEnd}
timezone={currentTimezone}
onFocus={getFocusHandler('shiftEnd')}
onBlur={handleBlur}
/>
</Field>
</div>
<UserGroups
value={userGroups}
onChange={setUserGroups}
isMultipleGroups={true}
renderUser={renderUser}
showError={!isFormValid}
/>
</VerticalGroup>
</div>
<HorizontalGroup justify="space-between">
<Text type="secondary">Timezone: {getTzOffsetString(dayjs().tz(currentTimezone))}</Text>
<HorizontalGroup>
<Button variant="secondary" onClick={onHide}>
{shiftId === 'new' ? 'Cancel' : 'Close'}
</Button>
<Button variant="primary" onClick={handleCreate} disabled={!isFormValid}>
{shiftId === 'new' ? 'Create' : 'Update'}
</Button>
</HorizontalGroup>
</HorizontalGroup>
</VerticalGroup>
</>
{showDeleteRotationConfirmation && (
<GrafanaModal
isOpen

View file

@ -41,6 +41,8 @@
font-size: 12px;
font-weight: 500;
pointer-events: none;
position: absolute;
white-space: nowrap;
}
.label {
@ -54,7 +56,6 @@
font-weight: bold;
margin-right: 5px;
flex-shrink: 0;
pointer-events: none;
}
.details {

View file

@ -123,12 +123,14 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
duration={duration}
/>
)}
{userIndex === 0 && label && (
<div className={cx('label')} style={{ color }}>
{label}
</div>
)}
<div className={cx('title')}>{title}</div>
<div className={cx('title')}>
{userIndex === 0 && label && (
<div className={cx('label')} style={{ color }}>
{label}
</div>
)}
{title}
</div>
</div>
</Tooltip>
);

View file

@ -1,5 +1,9 @@
/* Navigation/Layout */
.drawer-content {
overflow: auto !important; /* fix https://github.com/grafana/oncall/issues/415 */
}
.page-body {
max-width: unset !important;
}

View file

@ -147,6 +147,8 @@ export class ScheduleStore extends BaseStore {
...this.items,
[item.id]: item,
};
return item;
}
}

View file

@ -58,6 +58,14 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
this.update().then(() => this.parseQueryParams(true));
}
componentDidUpdate(prevProps: Readonly<IntegrationsProps>): void {
if (prevProps.match.params.id && !this.props.match.params.id) {
this.setState({ errorData: initErrorDataState() }, () => {
this.parseQueryParams();
});
}
}
setSelectedAlertReceiveChannel = (alertReceiveChannelId: AlertReceiveChannel['id'], shouldRedirect = false) => {
const { store, history } = this.props;
store.selectedAlertReceiveChannel = alertReceiveChannelId;

View file

@ -27,3 +27,8 @@
position: relative;
width: 100%;
}
.not-found {
margin: 50px auto;
text-align: center;
}

View file

@ -6,7 +6,11 @@ import dayjs from 'dayjs';
import { observer } from 'mobx-react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import PageErrorHandlingWrapper from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import {
getWrongTeamResponseInfo,
initErrorDataState,
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
import PluginLink from 'components/PluginLink/PluginLink';
import ScheduleWarning from 'components/ScheduleWarning/ScheduleWarning';
import Text from 'components/Text/Text';
@ -33,7 +37,7 @@ const cx = cn.bind(styles);
interface SchedulePageProps extends PageProps, WithStoreProps, RouteComponentProps<{ id: string }> {}
interface SchedulePageState {
interface SchedulePageState extends PageBaseState {
startMoment: dayjs.Dayjs;
schedulePeriodType: string;
renderType: string;
@ -59,6 +63,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
isLoading: true,
showEditForm: false,
showScheduleICalSettings: false,
errorData: initErrorDataState(),
};
}
@ -101,8 +106,11 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
shiftIdToShowOverridesForm,
showEditForm,
showScheduleICalSettings,
errorData,
} = this.state;
const { isNotFoundError } = errorData;
const { scheduleStore, currentTimezone } = store;
const users = store.userStore.getSearchResult().results;
@ -115,131 +123,147 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
shiftIdToShowOverridesForm;
return (
<PageErrorHandlingWrapper pageName="schedules">
<PageErrorHandlingWrapper errorData={errorData} objectName="schedule" pageName="schedules">
{() => (
<>
<div className={cx('root')}>
<VerticalGroup spacing="lg">
<div className={cx('header')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<PluginLink query={{ page: 'schedules' }}>
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xl" />
</PluginLink>
<Text.Title
editable
editModalTitle="Schedule name"
level={2}
onTextChange={this.handleNameChange}
>
{schedule?.name}
</Text.Title>
{schedule && <ScheduleWarning item={schedule} />}
</HorizontalGroup>
<HorizontalGroup spacing="lg">
{users && (
<HorizontalGroup>
<Text type="secondary">Current timezone:</Text>
<UserTimezoneSelect
value={currentTimezone}
users={users}
onChange={this.handleTimezoneChange}
/>
</HorizontalGroup>
)}
<HorizontalGroup>
<HorizontalGroup>
<HorizontalGroup>
<Button variant="secondary" onClick={this.handleExportClick()}>
Export
</Button>
</HorizontalGroup>
{(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && (
<Button variant="secondary" onClick={this.handleReloadClick(scheduleId)}>
Reload
</Button>
)}
</HorizontalGroup>
<ToolbarButton
icon="cog"
tooltip="Settings"
onClick={() => {
this.setState({ showEditForm: true });
}}
/>
<WithConfirm>
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
</WithConfirm>
</HorizontalGroup>
</HorizontalGroup>
</HorizontalGroup>
{isNotFoundError ? (
<div className={cx('not-found')}>
<VerticalGroup spacing="lg" align="center">
<Text.Title level={1}>404</Text.Title>
<Text.Title level={4}>Schedule not found</Text.Title>
<PluginLink query={{ page: 'schedules' }}>
<Button variant="secondary" icon="arrow-left" size="md">
Go to Schedules page
</Button>
</PluginLink>
</VerticalGroup>
</div>
<div className={cx('users-timezones')}>
<UsersTimezones
scheduleId={scheduleId}
startMoment={startMoment}
onCallNow={schedule?.on_call_now || []}
userIds={
scheduleStore.relatedUsers[scheduleId] ? Object.keys(scheduleStore.relatedUsers[scheduleId]) : []
}
tz={currentTimezone}
onTzChange={this.handleTimezoneChange}
/>
</div>
<div className={cx('rotations')}>
<div className={cx('controls')}>
) : (
<VerticalGroup spacing="lg">
<div className={cx('header')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<Button variant="secondary" onClick={this.handleTodayClick}>
Today
</Button>
<HorizontalGroup spacing="xs">
<Button variant="secondary" onClick={this.handleLeftClick}>
<Icon name="angle-left" />
</Button>
<Button variant="secondary" onClick={this.handleRightClick}>
<Icon name="angle-right" />
</Button>
</HorizontalGroup>
<Text.Title style={{ marginLeft: '8px' }} level={4} type="primary">
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
<PluginLink query={{ page: 'schedules' }}>
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xl" />
</PluginLink>
<Text.Title
editable
editModalTitle="Schedule name"
level={2}
onTextChange={this.handleNameChange}
>
{schedule?.name}
</Text.Title>
{schedule && <ScheduleWarning item={schedule} />}
</HorizontalGroup>
<HorizontalGroup spacing="lg">
{users && (
<HorizontalGroup>
<Text type="secondary">Current timezone:</Text>
<UserTimezoneSelect
value={currentTimezone}
users={users}
onChange={this.handleTimezoneChange}
/>
</HorizontalGroup>
)}
<HorizontalGroup>
<HorizontalGroup>
<HorizontalGroup>
<Button variant="secondary" onClick={this.handleExportClick()}>
Export
</Button>
</HorizontalGroup>
{(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && (
<Button variant="secondary" onClick={this.handleReloadClick(scheduleId)}>
Reload
</Button>
)}
</HorizontalGroup>
<ToolbarButton
icon="cog"
tooltip="Settings"
onClick={() => {
this.setState({ showEditForm: true });
}}
/>
<WithConfirm>
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
</WithConfirm>
</HorizontalGroup>
</HorizontalGroup>
</HorizontalGroup>
</div>
<ScheduleFinal
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onClick={this.handleShowForm}
disabled={disabled}
/>
<Rotations
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onCreate={this.handleCreateRotation}
onUpdate={this.handleUpdateRotation}
onDelete={this.handleDeleteRotation}
shiftIdToShowRotationForm={shiftIdToShowRotationForm}
onShowRotationForm={this.handleShowRotationForm}
disabled={disabled}
/>
<ScheduleOverrides
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onCreate={this.handleCreateOverride}
onUpdate={this.handleUpdateOverride}
onDelete={this.handleDeleteOverride}
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
onShowRotationForm={this.handleShowOverridesForm}
disabled={disabled}
/>
</div>
</VerticalGroup>
<div className={cx('users-timezones')}>
<UsersTimezones
scheduleId={scheduleId}
startMoment={startMoment}
onCallNow={schedule?.on_call_now || []}
userIds={
scheduleStore.relatedUsers[scheduleId]
? Object.keys(scheduleStore.relatedUsers[scheduleId])
: []
}
tz={currentTimezone}
onTzChange={this.handleTimezoneChange}
/>
</div>
<div className={cx('rotations')}>
<div className={cx('controls')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<Button variant="secondary" onClick={this.handleTodayClick}>
Today
</Button>
<HorizontalGroup spacing="xs">
<Button variant="secondary" onClick={this.handleLeftClick}>
<Icon name="angle-left" />
</Button>
<Button variant="secondary" onClick={this.handleRightClick}>
<Icon name="angle-right" />
</Button>
</HorizontalGroup>
<Text.Title style={{ marginLeft: '8px' }} level={4} type="primary">
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
</Text.Title>
</HorizontalGroup>
</HorizontalGroup>
</div>
<ScheduleFinal
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onClick={this.handleShowForm}
disabled={disabled}
/>
<Rotations
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onCreate={this.handleCreateRotation}
onUpdate={this.handleUpdateRotation}
onDelete={this.handleDeleteRotation}
shiftIdToShowRotationForm={shiftIdToShowRotationForm}
onShowRotationForm={this.handleShowRotationForm}
disabled={disabled}
/>
<ScheduleOverrides
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onCreate={this.handleCreateOverride}
onUpdate={this.handleUpdateOverride}
onDelete={this.handleDeleteOverride}
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
onShowRotationForm={this.handleShowOverridesForm}
disabled={disabled}
/>
</div>
</VerticalGroup>
)}
</div>
{showEditForm && schedule && (
<ScheduleForm
@ -325,7 +349,9 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
const { startMoment } = this.state;
store.scheduleStore.updateItem(scheduleId); // to refresh current oncall users
store.scheduleStore
.updateItem(scheduleId) // to refresh current oncall users
.catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } }));
store.scheduleStore.updateRelatedUsers(scheduleId); // to refresh related users
return Promise.all([

View file

@ -1,7 +1,6 @@
.schedule {
position: relative;
margin: 20px 0;
max-width: calc(100vw - 104px);
}
.title {