Merge pull request #617 from grafana/add-settings-button
New schedules 2nd bunch of fixes
This commit is contained in:
commit
680e11bb64
18 changed files with 483 additions and 201 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = (
|
|||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@
|
|||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.root .buttons {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
.root .expanded-row {
|
||||
background: var(--secondary-background);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue