Merge pull request #617 from grafana/add-settings-button

New schedules 2nd bunch of fixes
This commit is contained in:
Maxim Mordasov 2022-10-13 15:32:24 +01:00 committed by GitHub
commit 680e11bb64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 483 additions and 201 deletions

View file

@ -17,17 +17,21 @@
border: var(--border-weak);
box-shadow: var(--shadows-z3);
border-radius: 2px;
z-index: 10;
}
/*
.overlay {
position: fixed;
position: relative;
inset: 0;
z-index: 10;
/* background-color: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(1px); */
background-color: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(1px);
}
.body-open {
overflow: hidden;
}
*/

View file

@ -39,7 +39,8 @@ const Modal: FC<PropsWithChildren<ModalProps>> = (props) => {
contentLabel={title}
className={cx('root')}
overlayClassName={cx('overlay')}
bodyOpenClassName={cx('body-open')}
overlayElement={(props, contentElement) => contentElement} // render without overlay to allow body scroll
/* bodyOpenClassName={cx('body-open')} */
contentElement={contentElement}
>
{children}

View file

@ -6,6 +6,10 @@
width: 100%;
}
.root table tbody tr.row-even {
background: var(--background-secondary);
}
.root tr {
min-height: 56px;
}
@ -20,7 +24,8 @@
.root td {
min-height: 60px;
padding: 10px 0;
padding-top: 10px;
padding-bottom: 10px;
}
.pagination {
@ -37,6 +42,11 @@
transition: transform 0.2s;
}
/* to allow expand on expand-button click */
.root table :global(.rc-table-row-expand-icon-cell) > span {
pointer-events: none;
}
.expand-icon__expanded {
transform: rotate(0deg);
}

View file

@ -37,24 +37,31 @@ const GTable: FC<Props> = (props) => {
const { page, total: numberOfPages, onChange: onNavigate } = pagination || {};
if (expandable) {
expandable.expandIcon = ({ expanded, record }) => {
return (
<div className={cx('expand-icon', { [`expand-icon__expanded`]: expanded })}>
<ExpandIcon />
</div>
);
};
}
const expandableFn = useMemo(() => {
return expandable
? {
...expandable,
expandIcon: ({ expanded, record }) => {
return (
<div className={cx('expand-icon', { [`expand-icon__expanded`]: expanded })}>
<ExpandIcon />
</div>
);
},
expandedRowClassName: (record, index) => (index % 2 === 0 ? cx('row-even') : cx('row-odd')),
}
: null;
}, [expandable]);
return (
<VerticalGroup justify="flex-end">
<Table
rowKey={rowKey}
className={cx('root', 'filter-table', className)}
className={cx('root', className)}
columns={columns}
data={data}
expandable={expandable}
expandable={expandableFn}
rowClassName={(record, index) => (index % 2 === 0 ? cx('row-even') : cx('row-odd'))}
{...restProps}
/>
{pagination && (

View file

@ -17,6 +17,8 @@ const cx = cn.bind(styles);
const TimelineMarks: FC<TimelineMarksProps> = (props) => {
const { startMoment, debug } = props;
const currentMoment = useMemo(() => dayjs(), []);
const momentsToRender = useMemo(() => {
const hoursToSplit = 12;
@ -60,10 +62,14 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
</svg>
)}
{momentsToRender.map((m, i) => {
const isCurrentDay = currentMoment.isSame(m.moment, 'day');
return (
<div key={i} className={cx('weekday')}>
<div className={cx('weekday-title')}>
<Text type="secondary">{m.moment.format('ddd D MMM')}</Text>
<Text type="secondary" strong={isCurrentDay}>
{m.moment.format('ddd D MMM')}
</Text>
</div>
<div className={cx('weekday-times')}>
{m.moments.map((mm, j) => (

View file

@ -20,7 +20,9 @@ const WithConfirm = (props: WithConfirmProps) => {
const [showConfirmation, setShowConfirmation] = useState<boolean>(false);
const onClickCallback = useCallback(() => {
const onClickCallback = useCallback((event) => {
event.stopPropagation();
setShowConfirmation(true);
}, []);

View file

@ -44,23 +44,19 @@ const DateTimePicker = (props: UserTooltipProps) => {
const minDate = useMemo(() => (minMoment ? toDate(minMoment, timezone) : undefined), [minMoment, timezone]);
const handleDateChange = useCallback(
(newDate: Date) => {
const localMoment = dayjs().tz(timezone).utcOffset() === 0 ? dayjs().utc() : dayjs().tz(timezone);
const handleDateChange = (newDate: Date) => {
const localMoment = dayjs().tz(timezone).utcOffset() === 0 ? dayjs().utc() : dayjs().tz(timezone);
const newValue = localMoment
.set('year', newDate.getFullYear())
.set('month', newDate.getMonth())
.set('date', newDate.getDate())
.set('hour', value.getHours())
.set('minute', value.getMinutes())
.set('second', value.getSeconds());
onChange(newValue);
},
[value]
);
const newValue = localMoment
.set('year', newDate.getFullYear())
.set('month', newDate.getMonth())
.set('date', newDate.getDate())
.set('hour', value.getHours())
.set('minute', value.getMinutes())
.set('second', value.getSeconds());
onChange(newValue);
};
const handleTimeChange = useCallback(
(newMoment: DateTime) => {
const localMoment = dayjs().tz(timezone).utcOffset() === 0 ? dayjs().utc() : dayjs().tz(timezone);

View file

@ -96,6 +96,16 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
}
}, [rotationStart, shiftStart]);
const updateShiftStart = useCallback(
(value) => {
const diff = shiftEnd.diff(shiftStart);
setShiftStart(value);
setShiftEnd(value.add(diff));
},
[shiftStart, shiftEnd]
);
const store = useStore();
const shift = store.scheduleStore.shifts[shiftId];
@ -247,8 +257,6 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
const isFormValid = useMemo(() => userGroups.some((group) => group.length), [userGroups]);
const moment = dayjs();
return (
<Modal
isOpen={isOpen}
@ -355,17 +363,17 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
className={cx('date-time-picker')}
label={
<Text type="primary" size="small">
Shift start
Parent shift start
</Text>
}
>
<DateTimePicker value={shiftStart} onChange={setShiftStart} timezone={currentTimezone} />
<DateTimePicker value={shiftStart} onChange={updateShiftStart} timezone={currentTimezone} />
</Field>
<Field
className={cx('date-time-picker')}
label={
<Text type="primary" size="small">
Shift end
Parent shift end
</Text>
}
>

View file

@ -37,6 +37,7 @@ interface RotationsProps extends WithStoreProps {
onCreate: () => void;
onUpdate: () => void;
onDelete: () => void;
disabled: boolean;
}
interface RotationsState {
@ -52,7 +53,17 @@ class Rotations extends Component<RotationsProps, RotationsState> {
};
render() {
const { scheduleId, startMoment, currentTimezone, onCreate, onUpdate, onDelete, store, shiftIdToShowRotationForm } = this.props;
const {
scheduleId,
startMoment,
currentTimezone,
onCreate,
onUpdate,
onDelete,
store,
shiftIdToShowRotationForm,
disabled,
} = this.props;
const { layerPriority, shiftMomentToShowRotationForm } = this.state;
const base = 7 * 24 * 60; // in minutes
@ -87,13 +98,19 @@ class Rotations extends Component<RotationsProps, RotationsState> {
Rotations
</Text.Title>
</div>
<ValuePicker
label="Add rotation"
options={options}
onChange={this.handleAddRotation}
variant="primary"
size="md"
/>
{disabled ? (
<Button variant="primary" icon="plus" disabled>
Add rotation
</Button>
) : (
<ValuePicker
label="Add rotation"
options={options}
onChange={this.handleAddRotation}
variant="primary"
size="md"
/>
)}
</HorizontalGroup>
</div>
<div className={cx('rotations-plus-title')}>
@ -217,19 +234,35 @@ class Rotations extends Component<RotationsProps, RotationsState> {
}
onRotationClick = (shiftId: Shift['id'], moment?: dayjs.Dayjs) => {
const { disabled } = this.props;
if (disabled) {
return;
}
this.setState({ shiftMomentToShowRotationForm: moment }, () => {
this.onShowRotationForm(shiftId);
});
};
handleAddLayer = (layerPriority: number, moment?: dayjs.Dayjs) => {
const { disabled } = this.props;
if (disabled) {
return;
}
this.setState({ layerPriority, shiftMomentToShowRotationForm: moment }, () => {
this.onShowRotationForm('new');
});
};
handleAddRotation = (option: SelectableValue) => {
const { startMoment } = this.props;
const { startMoment, disabled } = this.props;
if (disabled) {
return;
}
this.setState(
{

View file

@ -38,6 +38,7 @@ interface ScheduleOverridesProps extends WithStoreProps {
onCreate: () => void;
onUpdate: () => void;
onDelete: () => void;
disabled: boolean;
}
interface ScheduleOverridesState {
@ -51,8 +52,17 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
};
render() {
const { startMoment, currentTimezone, onCreate, onUpdate, onDelete, store, shiftIdToShowRotationForm, scheduleId } =
this.props;
const {
scheduleId,
startMoment,
currentTimezone,
onCreate,
onUpdate,
onDelete,
store,
shiftIdToShowRotationForm,
disabled,
} = this.props;
const { shiftMomentToShowOverrideForm } = this.state;
const shifts = getOverridesFromStore(store, scheduleId, startMoment) as ShiftEvents[];
@ -74,7 +84,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
Overrides
</Text.Title>
</div>
<Button icon="plus" onClick={this.handleAddOverride} variant="secondary">
<Button disabled={disabled} icon="plus" onClick={this.handleAddOverride} variant="secondary">
Add override
</Button>
</HorizontalGroup>
@ -154,13 +164,23 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
}
onRotationClick = (shiftId: Shift['id'], moment: dayjs.Dayjs) => {
const { disabled } = this.props;
if (disabled) {
return;
}
this.setState({ shiftMomentToShowOverrideForm: moment }, () => {
this.onShowRotationForm(shiftId);
});
};
handleAddOverride = () => {
const { startMoment } = this.props;
const { startMoment, disabled } = this.props;
if (disabled) {
return;
}
this.setState({ shiftMomentToShowOverrideForm: startMoment }, () => {
this.onShowRotationForm('new');

View file

@ -6,14 +6,16 @@ import { DEFAULT_USER_ROLES } from 'models/user/user.config';
const commonFields: FormItem[] = [
{
name: 'ical_url_overrides',
label: 'Overrides schedule iCal URL ',
type: FormItemType.TextArea,
description:
'You can use an override calendar to share with your team members. Users can add \n' +
'events to this calendar, and they will override existing events in the primary \n' +
'calendar. The iCal URL for your override calendar can be found in the calendar \n' +
'integration settings of your calendar service.',
name: 'team',
label: 'Assign to team',
type: FormItemType.GSelect,
extra: {
modelName: 'grafanaTeamStore',
displayField: 'name',
valueField: 'id',
showSearch: true,
allowClear: true,
},
},
{
name: 'slack_channel_id',
@ -29,6 +31,19 @@ const commonFields: FormItem[] = [
description:
'Calendar parsing errors and notifications about the new on-call shift will be published in this channel.',
},
{
name: 'user_group',
label: 'Slack user group',
type: FormItemType.GSelect,
extra: {
modelName: 'userGroupStore',
displayField: 'handle',
showSearch: true,
allowClear: true,
},
description:
'Group members will be automatically updated with current on-call. In case you want to ping on-call with @group_name.',
},
{
name: 'notify_oncall_shift_freq',
label: 'Notification frequency',
@ -77,37 +92,12 @@ const commonFields: FormItem[] = [
},
description: 'Specify how to notify a team member when their shift is the next one scheduled',
},
{
name: 'user_group',
label: 'Slack user group',
type: FormItemType.GSelect,
extra: {
modelName: 'userGroupStore',
displayField: 'handle',
showSearch: true,
allowClear: true,
},
description:
'Group members will be automatically updated with current on-call. In case you want to ping on-call with @group_name.',
},
// {
// name: 'send_empty_shifts_report',
// normalize: (value) => Boolean(value),
// label: 'Send reports about empty shifts to Slack',
// type: FormItemType.Switch,
// },
{
name: 'team',
label: 'Assign to team',
type: FormItemType.GSelect,
extra: {
modelName: 'grafanaTeamStore',
displayField: 'name',
valueField: 'id',
showSearch: true,
allowClear: true,
},
},
];
export const iCalForm: { name: string; fields: FormItem[] } = {
@ -128,6 +118,16 @@ export const iCalForm: { name: string; fields: FormItem[] } = {
'access. The iCal URL for your primary calendar can be found in the calendar \n' +
'integration settings of your calendar service.',
},
{
name: 'ical_url_overrides',
label: 'Overrides schedule iCal URL ',
type: FormItemType.TextArea,
description:
'You can use an override calendar to share with your team members. Users can add \n' +
'events to this calendar, and they will override existing events in the primary \n' +
'calendar. The iCal URL for your override calendar can be found in the calendar \n' +
'integration settings of your calendar service.',
},
...commonFields,
],
};
@ -140,6 +140,16 @@ export const calendarForm: { name: string; fields: FormItem[] } = {
type: FormItemType.Input,
validation: { required: true },
},
{
name: 'ical_url_overrides',
label: 'Overrides schedule iCal URL ',
type: FormItemType.TextArea,
description:
'You can use an override calendar to share with your team members. Users can add \n' +
'events to this calendar, and they will override existing events in the primary \n' +
'calendar. The iCal URL for your override calendar can be found in the calendar \n' +
'integration settings of your calendar service.',
},
...commonFields,
],
};
@ -152,5 +162,6 @@ export const apiForm: { name: string; fields: FormItem[] } = {
type: FormItemType.Input,
validation: { required: true },
},
...commonFields,
],
};

View file

@ -78,10 +78,10 @@ const ScheduleForm = observer((props: ScheduleFormProps) => {
>
<div className={cx('content')}>
<VerticalGroup>
<Text type="secondary">
{/*<Text type="secondary">
Manage on-call schedules using your favourite calendar app, such as Google Calendar or Microsoft Outlook. To
schedule on-call shifts create a new calendar and use events with the teammates usernames
</Text>
</Text>*/}
<GForm form={formConfig} data={data} onSubmit={handleSubmit} />
<WithPermissionControl userAction={UserAction.UpdateSchedules}>
<Button form={formConfig.name} type="submit">

View file

@ -1,5 +1,5 @@
.root {
border: var(--border-medium);
border: var(--border-weak);
display: flex;
flex-direction: column;
background: var(--background-secondary);
@ -15,7 +15,7 @@
font-size: 19px;
line-height: 24px;
color: rgba(204, 204, 220, 0.65);
margin: 16px 0;
margin-top: 16px;
}
.current-time {

View file

@ -2,7 +2,7 @@ import dayjs from 'dayjs';
import { findColor } from 'containers/Rotations/Rotations.helpers';
import { getLayersFromStore, getOverridesFromStore, getShiftsFromStore } from 'models/schedule/schedule.helpers';
import { Event } from 'models/schedule/schedule.types';
import { Event, Layer } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { RootStore } from 'state';
@ -18,7 +18,6 @@ export const getDateTime = (date: string) => {
return dayjs(date);
};
export const getColorSchemeMappingForUsers = (
store: RootStore,
scheduleId: string,
@ -26,15 +25,21 @@ export const getColorSchemeMappingForUsers = (
): { [userId: string]: Set<string> } => {
const usersColorSchemeHash: { [userId: string]: Set<string> } = {};
const shifts = getShiftsFromStore(store, scheduleId, startMoment);
const layers = getLayersFromStore(store, scheduleId, startMoment);
const finalScheduleShifts = getShiftsFromStore(store, scheduleId, startMoment);
const layers: Layer[] = getLayersFromStore(store, scheduleId, startMoment);
const overrides = getOverridesFromStore(store, scheduleId, startMoment);
if (!shifts?.length || !layers?.length) {
if (!finalScheduleShifts?.length || !layers?.length) {
return usersColorSchemeHash;
}
shifts.forEach(({ shiftId, events }) => populateUserHashSet(events, shiftId));
const rotationShifts = layers.reduce((prev, current) => {
prev.push(...current.shifts);
return prev;
}, []);
finalScheduleShifts.forEach(({ shiftId, events }) => populateUserHashSet(events, shiftId));
rotationShifts.forEach(({ shiftId, events }) => populateUserHashSet(events, shiftId));
return usersColorSchemeHash;
@ -49,4 +54,4 @@ export const getColorSchemeMappingForUsers = (
});
});
}
}
};

View file

@ -4,7 +4,7 @@
margin: 0 auto;
margin-top: 24px;
--rotations-border: var(--border-medium);
--rotations-border: var(--border-weak);
--rotations-background: var(--background-secondary);
}
@ -19,7 +19,6 @@
.users-timezones {
width: 100%;
margin-bottom: 16px;
}
.controls {
@ -29,7 +28,7 @@
.rotations {
display: flex;
flex-direction: column;
gap: 20px;
gap: 16px;
position: relative;
width: 100%;
}

View file

@ -14,8 +14,9 @@ import WithConfirm from 'components/WithConfirm/WithConfirm';
import Rotations from 'containers/Rotations/Rotations';
import ScheduleFinal from 'containers/Rotations/ScheduleFinal';
import ScheduleOverrides from 'containers/Rotations/ScheduleOverrides';
import ScheduleForm from 'containers/ScheduleForm/ScheduleForm';
import UsersTimezones from 'containers/UsersTimezones/UsersTimezones';
import { Shift } from 'models/schedule/schedule.types';
import { ScheduleType, Shift } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
@ -35,6 +36,7 @@ interface SchedulePageState {
shiftIdToShowRotationForm?: Shift['id'];
shiftIdToShowOverridesForm?: Shift['id'];
isLoading: boolean;
showEditForm: boolean;
}
const INITIAL_TIMEZONE = 'UTC'; // todo check why doesn't work
@ -52,6 +54,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
shiftIdToShowRotationForm: undefined,
shiftIdToShowOverridesForm: undefined,
isLoading: true,
showEditForm: false,
};
}
@ -82,107 +85,193 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
query: { id: scheduleId },
store,
} = this.props;
const { startMoment, shiftIdToShowRotationForm, shiftIdToShowOverridesForm } = this.state;
const {
startMoment,
shiftIdToShowRotationForm,
shiftIdToShowOverridesForm,
showEditForm,
} = this.state;
const { scheduleStore, currentTimezone } = store;
const users = store.userStore.getSearchResult().results;
const schedule = scheduleStore.items[scheduleId];
return (
<div className={cx('root')}>
<VerticalGroup spacing="lg">
<div className={cx('header')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<PluginLink query={{ page: 'schedules-new' }}>
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xxl" />
</PluginLink>
<Text.Title editable editModalTitle="Schedule name" level={2} onTextChange={this.handleNameChange}>
{schedule?.name}
</Text.Title>
</HorizontalGroup>
<HorizontalGroup>
{users && (
<HorizontalGroup>
<Text type="secondary">Current timezone:</Text>
<UserTimezoneSelect value={currentTimezone} users={users} onChange={this.handleTimezoneChange} />
</HorizontalGroup>
)}
<WithConfirm>
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
</WithConfirm>
</HorizontalGroup>
</HorizontalGroup>
</div>
<Text className={cx('desc')} size="small" type="secondary">
On-call Schedules. Use this to distribute notifications among team members you specified in the "Notify
Users from on-call schedule" step in escalation chains.
</Text>
<div className={cx('users-timezones')}>
<UsersTimezones
onCallNow={schedule?.on_call_now || []}
userIds={
scheduleStore.relatedUsers[scheduleId] ? Object.keys(scheduleStore.relatedUsers[scheduleId]) : []
}
tz={currentTimezone}
startMoment={this.state.startMoment}
onTzChange={this.handleTimezoneChange}
scheduleId={scheduleId}
/>
</div>
<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>
<>
<div className={cx('root')}>
<VerticalGroup spacing="lg">
<div className={cx('header')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<PluginLink query={{ page: 'schedules-new' }}>
<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>
{/*<ScheduleCounter
type="link"
count={5}
tooltipTitle="Used in escalations"
tooltipContent={
<>
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 1</PluginLink>
<br />
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 2</PluginLink>
<br />
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 3</PluginLink>
</>
}
/>
<ScheduleCounter
type="warning"
count={2}
tooltipTitle="Warnings"
tooltipContent="Schedule has unassigned time periods during next 7 days"
/>*/}
</HorizontalGroup>
<HorizontalGroup spacing="lg">
{users && (
<HorizontalGroup>
<Text type="secondary">Current timezone:</Text>
<UserTimezoneSelect value={currentTimezone} users={users} onChange={this.handleTimezoneChange} />
</HorizontalGroup>
)}
{/*<ScheduleQuality quality={0.89} />*/}
{/*<ToolbarButton icon="copy" tooltip="Copy" />
<ToolbarButton icon="brackets-curly" tooltip="Code" />
<ToolbarButton icon="share-alt" tooltip="Share" />
*/}
<HorizontalGroup>
<ToolbarButton
icon="cog"
tooltip="Settings"
onClick={() => {
this.setState({ showEditForm: true });
}}
/>
<WithConfirm>
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
</WithConfirm>
</HorizontalGroup>
</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>
<div className={cx('rotations')}>
<ScheduleFinal
currentTimezone={currentTimezone}
startMoment={startMoment}
onClick={this.handleShowForm}
scheduleId={scheduleId}
/>
<Rotations
currentTimezone={currentTimezone}
startMoment={startMoment}
onCreate={this.handleCreateRotation}
onUpdate={this.handleUpdateRotation}
onDelete={this.handleDeleteRotation}
shiftIdToShowRotationForm={shiftIdToShowRotationForm}
onShowRotationForm={this.handleShowRotationForm}
scheduleId={scheduleId}
/>
<ScheduleOverrides
currentTimezone={currentTimezone}
startMoment={startMoment}
onCreate={this.handleCreateOverride}
onUpdate={this.handleUpdateOverride}
onDelete={this.handleDeleteOverride}
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
onShowRotationForm={this.handleShowOverridesForm}
scheduleId={scheduleId}
/>
</div>
</VerticalGroup>
</div>
</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={'current-time'} />*/}
<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 width="auto">
<RadioButtonGroup
options={[
{ label: 'Day', value: 'day' },
{
label: 'Week',
value: 'week',
},
{ label: 'Month', value: 'month' },
{ label: 'Custom', value: 'custom' },
]}
value={schedulePeriodType}
onChange={this.handleShedulePeriodTypeChange}
/>
<RadioButtonGroup
options={[
{ label: 'Timeline', value: 'timeline' },
{
label: 'Grid',
value: 'grid',
},
]}
value={renderType}
onChange={this.handleRenderTypeChange}
/>
</HorizontalGroup>*/}
</HorizontalGroup>
</div>
<ScheduleFinal
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onClick={this.handleShowForm}
/>
<Rotations
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onCreate={this.handleCreateRotation}
onUpdate={this.handleUpdateRotation}
onDelete={this.handleDeleteRotation}
shiftIdToShowRotationForm={shiftIdToShowRotationForm}
onShowRotationForm={this.handleShowRotationForm}
disabled={shiftIdToShowRotationForm || shiftIdToShowOverridesForm}
/>
<ScheduleOverrides
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onCreate={this.handleCreateOverride}
onUpdate={this.handleUpdateOverride}
onDelete={this.handleDeleteOverride}
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
onShowRotationForm={this.handleShowOverridesForm}
disabled={shiftIdToShowRotationForm || shiftIdToShowOverridesForm}
/>
</div>
</VerticalGroup>
</div>
{showEditForm && (
<ScheduleForm
id={schedule.id}
onUpdate={this.update}
onHide={() => {
this.setState({ showEditForm: false });
}}
/>
)}
</>
);
}
update = () => {
const { store, query } = this.props;
const { id: scheduleId } = query;
const { scheduleStore } = store;
return scheduleStore.updateItem(scheduleId);
};
handleShowForm = async (shiftId: Shift['id'] | 'new') => {
const {
store: { scheduleStore },
@ -191,17 +280,29 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
const shift = await scheduleStore.updateOncallShift(shiftId);
if (shift.type === 2) {
this.setState({ shiftIdToShowRotationForm: shiftId });
this.handleShowRotationForm(shiftId);
} else if (shift.type === 3) {
this.setState({ shiftIdToShowOverridesForm: shiftId });
this.handleShowOverridesForm(shiftId);
}
};
handleShowRotationForm = (shiftId: Shift['id'] | 'new') => {
const { shiftIdToShowRotationForm, shiftIdToShowOverridesForm } = this.state;
if (shiftId && (shiftIdToShowRotationForm || shiftIdToShowOverridesForm)) {
return;
}
this.setState({ shiftIdToShowRotationForm: shiftId });
};
handleShowOverridesForm = (shiftId: Shift['id'] | 'new') => {
const { shiftIdToShowRotationForm, shiftIdToShowOverridesForm } = this.state;
if (shiftId && (shiftIdToShowRotationForm || shiftIdToShowOverridesForm)) {
return;
}
this.setState({ shiftIdToShowOverridesForm: shiftId });
};

View file

@ -15,6 +15,10 @@
padding-left: 20px;
}
.root .buttons {
padding-right: 10px;
}
/*
.root .expanded-row {
background: var(--secondary-background);

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { SyntheticEvent } from 'react';
import { getLocationSrv } from '@grafana/runtime';
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
@ -19,12 +19,16 @@ import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import ScheduleFinal from 'containers/Rotations/ScheduleFinal';
import ScheduleForm from 'containers/ScheduleForm/ScheduleForm';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Schedule, ScheduleType } from 'models/schedule/schedule.types';
import { getSlackChannelName } from 'models/slack_channel/slack_channel.helpers';
import { Timezone } from 'models/timezone/timezone.types';
import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
import { AppFeature } from 'state/features';
import { WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
import styles from './Schedules.module.css';
@ -38,6 +42,7 @@ interface SchedulesPageState {
filters: SchedulesFiltersType;
showNewScheduleSelector: boolean;
expandedRowKeys: Array<Schedule['id']>;
scheduleIdToEdit?: Schedule['id'];
}
@observer
@ -51,6 +56,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
filters: { searchTerm: '', status: 'all', type: ScheduleType.API },
showNewScheduleSelector: false,
expandedRowKeys: [],
scheduleIdToEdit: undefined,
};
}
@ -63,12 +69,18 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
render() {
const { store } = this.props;
const { filters, showNewScheduleSelector, expandedRowKeys } = this.state;
const { filters, showNewScheduleSelector, expandedRowKeys, scheduleIdToEdit } = this.state;
const { scheduleStore } = store;
const schedules = scheduleStore.getSearchResult(/*filters.searchTerm*/);
const columns = [
{
width: '10%',
title: 'Type',
dataIndex: 'type',
render: this.renderType,
},
{
width: '10%',
title: 'Status',
@ -76,21 +88,44 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
render: this.renderStatus,
},
{
width: '40%',
width: '30%',
title: 'Name',
key: 'name',
render: this.renderName,
},
{
width: '45%',
width: '30%',
title: 'Oncall',
key: 'users',
render: this.renderOncallNow,
},
{
width: '5%',
width: '10%',
title: 'Slack channel',
render: this.renderChannelName,
},
{
width: '10%',
title: 'Slack user group',
render: this.renderUserGroup,
},
/* {
width: '20%',
title: 'ChatOps',
key: 'chatops',
render: this.renderChatOps,
},*/
/*{
width: '10%',
title: 'Quality',
key: 'quality',
render: this.renderQuality,
},*/
{
width: '50px',
key: 'buttons',
render: this.renderButtons,
className: cx('buttons'),
},
];
@ -155,6 +190,15 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
}}
/>
)}
{scheduleIdToEdit && (
<ScheduleForm
id={scheduleIdToEdit}
onUpdate={this.update}
onHide={() => {
this.setState({ scheduleIdToEdit: undefined });
}}
/>
)}
</>
);
}
@ -227,6 +271,14 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
};
};
renderType = (value: number) => {
type tTypeToVerbal = {
[key: number]: string;
};
const typeToVerbal: tTypeToVerbal = { 0: 'API/Terraform', 1: 'Ical', 2: 'Web' };
return typeToVerbal[value];
};
renderStatus = (item: Schedule) => {
const {
store: { scheduleStore },
@ -293,6 +345,14 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
return null;
};
renderChannelName = (value: Schedule) => {
return getSlackChannelName(value.slack_channel) || '-';
};
renderUserGroup = (value: Schedule) => {
return value.user_group?.handle || '-';
};
/* renderChatOps = (item: Schedule) => {
return item.chatOps;
}; */
@ -309,18 +369,33 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
{/*<IconButton tooltip="Copy" name="copy" />
<IconButton tooltip="Settings" name="cog" />
<IconButton tooltip="Code" name="brackets-curly" />*/}
<WithConfirm>
<IconButton tooltip="Delete" name="trash-alt" onClick={this.getDeleteScheduleClickHandler(item.id)} />
</WithConfirm>
<WithPermissionControl key="edit" userAction={UserAction.UpdateSchedules}>
<IconButton tooltip="Settings" name="cog" onClick={this.getEditScheduleClickHandler(item.id)} />
</WithPermissionControl>
<WithPermissionControl key="edit" userAction={UserAction.UpdateSchedules}>
<WithConfirm>
<IconButton tooltip="Delete" name="trash-alt" onClick={this.getDeleteScheduleClickHandler(item.id)} />
</WithConfirm>
</WithPermissionControl>
</HorizontalGroup>
);
};
getEditScheduleClickHandler = (id: Schedule['id']) => {
return (event) => {
event.stopPropagation();
this.setState({ scheduleIdToEdit: id });
};
};
getDeleteScheduleClickHandler = (id: Schedule['id']) => {
const { store } = this.props;
const { scheduleStore } = store;
return () => {
return (event: SyntheticEvent) => {
event.stopPropagation();
scheduleStore.delete(id).then(this.update);
};
};