split rotation into layers
This commit is contained in:
parent
bece1dab38
commit
949b2d7618
17 changed files with 339 additions and 175 deletions
|
|
@ -45,14 +45,14 @@ const UserTimezoneSelect: FC<UserTimezoneSelectProps> = (props) => {
|
|||
|
||||
const selectValue = useMemo(() => {
|
||||
const user = users.find((user) => user.timezone === value);
|
||||
return user.pk;
|
||||
return user?.pk;
|
||||
}, [value, users]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ value }) => {
|
||||
const user = users.find((user) => user.pk === value);
|
||||
|
||||
onChange(user.timezone);
|
||||
onChange(user?.timezone);
|
||||
},
|
||||
[users]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
const { users, tz, onTzChange } = props;
|
||||
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [currentMoment, setCurrentMoment] = useState<dayjs.Dayjs>(dayjs().tz(tz).startOf('minute'));
|
||||
const [currentMoment, setCurrentMoment] = useState<dayjs.Dayjs>(dayjs().tz(tz));
|
||||
|
||||
const getAvatarClickHandler = useCallback((user) => {
|
||||
return () => {
|
||||
|
|
@ -74,10 +74,10 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<div className={cx('title')}>Team timezones</div>
|
||||
<HorizontalGroup>
|
||||
{/* <HorizontalGroup>
|
||||
<InlineSwitch transparent />
|
||||
Current schedule users only
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>*/}
|
||||
</HorizontalGroup>
|
||||
<div className={cx('timezone-select')}>
|
||||
<Text type="secondary">
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@
|
|||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
/* overflow: hidden; */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.root:first-child .timeline {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { dateTime, DateTime } from '@grafana/data';
|
||||
import {
|
||||
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import Draggable from 'react-draggable';
|
||||
|
||||
import Modal from 'components/Modal/Modal';
|
||||
|
|
@ -25,7 +26,7 @@ import { Rotation, Schedule, Shift } from 'models/schedule/schedule.types';
|
|||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { SelectOption } from 'state/types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
|
|
@ -45,19 +46,23 @@ interface RotationFormProps {
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const RotationForm: FC<RotationFormProps> = (props) => {
|
||||
const startOfDay = dayjs().startOf('day');
|
||||
|
||||
const RotationForm: FC<RotationFormProps> = observer((props) => {
|
||||
const { onHide, onCreate, currentTimezone, scheduleId, onUpdate, layerIndex, shiftId } = props;
|
||||
|
||||
const [repeatEveryValue, setRepeatEveryValue] = useState<number>(1);
|
||||
const [repeatEveryPeriod, setRepeatEveryPeriod] = useState<number>(0);
|
||||
const [selectedDays, setSelectedDays] = useState<string[]>([]);
|
||||
const [shiftStart, setShiftStart] = useState<DateTime>(dateTime('2022-07-26 12:00:00'));
|
||||
const [shiftEnd, setShiftEnd] = useState<DateTime>(dateTime('2022-07-26 19:00:00'));
|
||||
const [rotationStart, setRotationStart] = useState<DateTime>(dateTime('2022-07-26 12:00:00'));
|
||||
const [shiftStart, setShiftStart] = useState<DateTime>(dateTime(startOfDay.format('YYYY-MM-DD HH:mm:ss')));
|
||||
const [shiftEnd, setShiftEnd] = useState<DateTime>(dateTime(startOfDay.add(1, 'day').format('YYYY-MM-DD HH:mm:ss')));
|
||||
const [rotationStart, setRotationStart] = useState<DateTime>(dateTime(startOfDay.format('YYYY-MM-DD HH:mm:ss')));
|
||||
const [endLess, setEndless] = useState<boolean>(true);
|
||||
const [rotationEnd, setRotationEnd] = useState<DateTime>(dateTime('2022-08-26 12:00:00'));
|
||||
const [rotationEnd, setRotationEnd] = useState<DateTime>(
|
||||
dateTime(startOfDay.add(1, 'month').format('YYYY-MM-DD HH:mm:ss'))
|
||||
);
|
||||
|
||||
const [userGroups, setUserGroups] = useState([['U9XM1G7KTE3KW'], ['UYKS64M6C59XM']]);
|
||||
const [userGroups, setUserGroups] = useState([[]]);
|
||||
|
||||
const getUser = (pk: User['pk']) => {
|
||||
return {
|
||||
|
|
@ -77,19 +82,13 @@ const RotationForm: FC<RotationFormProps> = (props) => {
|
|||
|
||||
const shift = store.scheduleStore.shifts[shiftId];
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
/* console.log(
|
||||
repeatEveryValue,
|
||||
repeatEveryPeriod,
|
||||
selectedDays,
|
||||
shiftStart,
|
||||
shiftEnd,
|
||||
rotationStart,
|
||||
endLess,
|
||||
rotationEnd
|
||||
);
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (shiftId !== 'new') {
|
||||
store.scheduleStore.updateOncallShift(shiftId);
|
||||
}
|
||||
}, [shiftId]);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
const params = {
|
||||
title: 'Rotation ' + Math.floor(Math.random() * 100),
|
||||
rotation_start: getUTCString(rotationStart, currentTimezone),
|
||||
|
|
@ -97,17 +96,23 @@ const RotationForm: FC<RotationFormProps> = (props) => {
|
|||
shift_start: getUTCString(shiftStart, currentTimezone),
|
||||
shift_end: getUTCString(shiftEnd, currentTimezone),
|
||||
rolling_users: userGroups.filter((group) => group.length),
|
||||
interval: repeatEveryValue,
|
||||
frequency: repeatEveryPeriod,
|
||||
by_day: repeatEveryPeriod === 1 ? selectedDays : null,
|
||||
priority_level: layerIndex + 1,
|
||||
priority_level: shiftId === 'new' ? layerIndex + 1 : shift?.priority_level,
|
||||
};
|
||||
|
||||
// console.log('params', params);
|
||||
|
||||
store.scheduleStore.createRotation(scheduleId, false, params).then(() => {
|
||||
onHide();
|
||||
onCreate();
|
||||
});
|
||||
if (shiftId === 'new') {
|
||||
store.scheduleStore.createRotation(scheduleId, false, params).then(() => {
|
||||
onHide();
|
||||
onCreate();
|
||||
});
|
||||
} else {
|
||||
store.scheduleStore.updateRotation(shiftId, params).then(() => {
|
||||
onHide();
|
||||
onUpdate();
|
||||
});
|
||||
}
|
||||
}, [
|
||||
repeatEveryValue,
|
||||
repeatEveryPeriod,
|
||||
|
|
@ -121,6 +126,22 @@ const RotationForm: FC<RotationFormProps> = (props) => {
|
|||
layerIndex,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shift) {
|
||||
setRotationStart(getDateTime(shift.rotation_start));
|
||||
setRotationEnd(getDateTime(shift.until));
|
||||
setShiftStart(getDateTime(shift.shift_start));
|
||||
setShiftEnd(getDateTime(shift.shift_end));
|
||||
setEndless(!shift.until);
|
||||
|
||||
setRepeatEveryValue(shift.interval);
|
||||
setRepeatEveryPeriod(shift.frequency);
|
||||
setSelectedDays(shift.by_day);
|
||||
|
||||
setUserGroups(shift.rolling_users);
|
||||
}
|
||||
}, [shift]);
|
||||
|
||||
const handleChangeEndless = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEndless(!event.currentTarget.checked);
|
||||
|
|
@ -150,7 +171,12 @@ const RotationForm: FC<RotationFormProps> = (props) => {
|
|||
>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text size="medium">{shiftId === 'new' ? 'New Rotation' : shift?.title}</Text>
|
||||
<Text size="medium">
|
||||
<HorizontalGroup spacing="sm">
|
||||
<span>[L{shiftId === 'new' ? layerIndex + 1 : shift?.priority_level}]</span>
|
||||
{shiftId === 'new' ? 'New Rotation' : shift?.title}
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
<HorizontalGroup>
|
||||
<IconButton disabled variant="secondary" tooltip="Copy" name="copy" />
|
||||
<IconButton disabled variant="secondary" tooltip="Code" name="brackets-curly" />
|
||||
|
|
@ -176,6 +202,7 @@ const RotationForm: FC<RotationFormProps> = (props) => {
|
|||
{ label: '4', value: 4 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '6', value: 6 },
|
||||
{ label: '7', value: 7 },
|
||||
]}
|
||||
onChange={handleRepeatEveryValueChange}
|
||||
/>
|
||||
|
|
@ -265,14 +292,14 @@ const RotationForm: FC<RotationFormProps> = (props) => {
|
|||
<HorizontalGroup>
|
||||
<Button variant="secondary">+ Override</Button>
|
||||
<Button variant="primary" onClick={handleCreate}>
|
||||
Create
|
||||
{shiftId === 'new' ? 'Create' : 'Update'}
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
interface DaysSelectorProps {
|
||||
value: string[];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { dateTime, DateTime } from '@grafana/data';
|
||||
import {
|
||||
|
|
@ -24,7 +24,7 @@ import { Rotation, Schedule, Shift } from 'models/schedule/schedule.types';
|
|||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import { RotationCreateData } from './RotationForm.types';
|
||||
|
|
@ -42,13 +42,13 @@ interface RotationFormProps {
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const startOfDay = dayjs().startOf('day');
|
||||
|
||||
const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
||||
const { onHide, onCreate, currentTimezone, scheduleId, onUpdate, shiftId } = props;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const startOfDay = dayjs().startOf('day');
|
||||
|
||||
const [shiftStart, setShiftStart] = useState<DateTime>(dateTime(startOfDay.format('YYYY-MM-DD HH:mm:ss')));
|
||||
const [shiftEnd, setShiftEnd] = useState<DateTime>(
|
||||
dateTime(startOfDay.add(12, 'hours').format('YYYY-MM-DD HH:mm:ss'))
|
||||
|
|
@ -65,6 +65,21 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
|
||||
const shift = store.scheduleStore.shifts[shiftId];
|
||||
|
||||
useEffect(() => {
|
||||
if (shiftId !== 'new') {
|
||||
store.scheduleStore.updateOncallShift(shiftId);
|
||||
}
|
||||
}, [shiftId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shift) {
|
||||
setShiftStart(getDateTime(shift.shift_start));
|
||||
setShiftEnd(getDateTime(shift.shift_end));
|
||||
|
||||
setUserGroups(shift.rolling_users);
|
||||
}
|
||||
}, [shift]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
store.scheduleStore.deleteOncallShift(shiftId).then(() => {
|
||||
onHide();
|
||||
|
|
@ -73,19 +88,26 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
}, []);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
store.scheduleStore
|
||||
.createRotation(scheduleId, true, {
|
||||
title: 'Override ' + Math.floor(Math.random() * 100),
|
||||
rotation_start: getUTCString(shiftStart, currentTimezone),
|
||||
shift_start: getUTCString(shiftStart, currentTimezone),
|
||||
shift_end: getUTCString(shiftEnd, currentTimezone),
|
||||
rolling_users: userGroups,
|
||||
frequency: null,
|
||||
})
|
||||
.then(() => {
|
||||
const params = {
|
||||
title: 'Override ' + Math.floor(Math.random() * 100),
|
||||
rotation_start: getUTCString(shiftStart, currentTimezone),
|
||||
shift_start: getUTCString(shiftStart, currentTimezone),
|
||||
shift_end: getUTCString(shiftEnd, currentTimezone),
|
||||
rolling_users: userGroups,
|
||||
frequency: null,
|
||||
};
|
||||
|
||||
if (shiftId === 'new') {
|
||||
store.scheduleStore.createRotation(scheduleId, true, params).then(() => {
|
||||
onHide();
|
||||
onCreate();
|
||||
});
|
||||
} else {
|
||||
store.scheduleStore.updateRotation(shiftId, params).then(() => {
|
||||
onHide();
|
||||
onUpdate();
|
||||
});
|
||||
}
|
||||
}, [shiftStart, shiftEnd, userGroups]);
|
||||
|
||||
return (
|
||||
|
|
@ -142,7 +164,7 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
<Text type="secondary">Timezone: {getTzOffsetString(dayjs().tz(currentTimezone))}</Text>
|
||||
<HorizontalGroup>
|
||||
<Button variant="primary" onClick={handleCreate}>
|
||||
Save
|
||||
{shiftId === 'new' ? 'Create' : 'Update'}
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
|
||||
/* transition: left 500ms ease; */
|
||||
transition: left 500ms ease;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
@ -36,6 +35,10 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.rotations {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layer-title {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
|
|
@ -80,4 +83,9 @@
|
|||
text-align: center;
|
||||
padding: 12px;
|
||||
color: rgba(204, 204, 220, 0.65);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-rotations-layer:hover {
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import Rotation from 'containers/Rotation/Rotation';
|
|||
import RotationForm from 'containers/RotationForm/RotationForm';
|
||||
import { RotationCreateData } from 'containers/RotationForm/RotationForm.types';
|
||||
import { getFromString } from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift } from 'models/schedule/schedule.types';
|
||||
import { Event, Schedule, Shift } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { SelectOption, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
||||
import { getColor, getLabel, getRandomTimeslots, getRandomUser } from './Rotations.helpers';
|
||||
|
|
@ -31,12 +31,14 @@ interface RotationsProps extends WithStoreProps {
|
|||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
type Layer = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
interface RotationsState {
|
||||
shiftIdToShowRotationForm?: Shift['id'];
|
||||
layerIndexToShowRotationForm?: number;
|
||||
}
|
||||
|
||||
interface Layer {
|
||||
priority: Shift['priority_level'];
|
||||
shifts: Array<{ shiftId: Shift['id']; events: Event[] }>;
|
||||
}
|
||||
|
||||
@observer
|
||||
|
|
@ -47,24 +49,51 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
|
||||
render() {
|
||||
const { scheduleId, startMoment, currentTimezone, onCreate, onUpdate, store, onClick } = this.props;
|
||||
const { shiftIdToShowRotationForm } = this.state;
|
||||
|
||||
const layers = [
|
||||
{ id: 1, title: 'Layer 1' },
|
||||
/*{ id: 1, title: 'Layer 2' },
|
||||
{ id: 2, title: 'Layer 3' },
|
||||
{ id: 3, title: 'Layer 4' }*/
|
||||
];
|
||||
const { shiftIdToShowRotationForm, layerIndexToShowRotationForm } = this.state;
|
||||
|
||||
const base = 7 * 24 * 60; // in minutes
|
||||
const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes');
|
||||
|
||||
const currentTimeX = diff / base;
|
||||
|
||||
const rotations = [{} /* {}*/];
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const shifts = store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)];
|
||||
|
||||
const layers: Layer[] | undefined = shifts
|
||||
? shifts
|
||||
.reduce((memo, shift) => {
|
||||
const storeShift = store.scheduleStore.shifts[shift.shiftId];
|
||||
let layer = memo.find((level) => level.priority === storeShift.priority_level);
|
||||
if (!layer) {
|
||||
layer = { priority: storeShift.priority_level, shifts: [] };
|
||||
memo.push(layer);
|
||||
}
|
||||
layer.shifts.push(shift);
|
||||
|
||||
return memo;
|
||||
}, [])
|
||||
.sort((a, b) => {
|
||||
if (a.priority > b.priority) {
|
||||
return 1;
|
||||
}
|
||||
if (a.priority < b.priority) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const options = layers
|
||||
? layers.map((layer) => ({
|
||||
label: `Layer ${layer.priority}`,
|
||||
value: layer.priority - 1,
|
||||
}))
|
||||
: [];
|
||||
|
||||
options.push({ label: 'New Layer', value: layers?.length || 0 });
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
|
|
@ -73,10 +102,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
<div className={cx('title')}>Rotations</div>
|
||||
<ValuePicker
|
||||
label="Add rotation"
|
||||
options={layers.map(({ title, id }) => ({
|
||||
label: title,
|
||||
value: id,
|
||||
}))}
|
||||
options={options}
|
||||
onChange={this.handleAddRotation}
|
||||
variant="secondary"
|
||||
size="md"
|
||||
|
|
@ -84,30 +110,36 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('rotations-plus-title')}>
|
||||
{shifts && shifts.length ? (
|
||||
shifts.map(({ shiftId, events }, layerIndex) => (
|
||||
<div key={layerIndex}>
|
||||
{layers && layers.length ? (
|
||||
layers.map((layer) => (
|
||||
<div key={layer.priority}>
|
||||
<div className={cx('layer')}>
|
||||
<div className={cx('layer-title')}>
|
||||
<HorizontalGroup spacing="sm" justify="center">
|
||||
Layer {layerIndex + 1} <Icon name="info-circle" />
|
||||
Layer {layer.priority} <Icon name="info-circle" />
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
<TimelineMarks debug startMoment={startMoment} />
|
||||
<div className={cx('rotations')}>
|
||||
<Rotation
|
||||
onClick={() => {
|
||||
this.onRotationClick(shiftId);
|
||||
}}
|
||||
events={events}
|
||||
layerIndex={layerIndex}
|
||||
rotationIndex={0}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
/>
|
||||
</div>
|
||||
<div className={cx('rotations')}>
|
||||
<TimelineMarks startMoment={startMoment} />
|
||||
{layer.shifts.map(({ shiftId, events }, rotationIndex) => (
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && (
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
)}
|
||||
<div>
|
||||
<Rotation
|
||||
onClick={() => {
|
||||
this.onRotationClick(shiftId);
|
||||
}}
|
||||
events={events}
|
||||
layerIndex={layer.priority - 1}
|
||||
rotationIndex={rotationIndex}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -140,7 +172,12 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className={cx('add-rotations-layer')} onClick={this.handleAddLayer}>
|
||||
<div
|
||||
className={cx('add-rotations-layer')}
|
||||
onClick={() => {
|
||||
this.handleAddLayer(layers ? layers.length : 0);
|
||||
}}
|
||||
>
|
||||
Add rotations layer +
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -149,7 +186,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
<RotationForm
|
||||
shiftId={shiftIdToShowRotationForm}
|
||||
scheduleId={scheduleId}
|
||||
layerIndex={shifts ? shifts.length : 0}
|
||||
layerIndex={layerIndexToShowRotationForm}
|
||||
currentTimezone={currentTimezone}
|
||||
onHide={() => {
|
||||
this.setState({ shiftIdToShowRotationForm: undefined });
|
||||
|
|
@ -168,10 +205,12 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
|
||||
updateEvents = () => {};
|
||||
|
||||
handleAddLayer = () => {};
|
||||
handleAddLayer = (layerIndex: number) => {
|
||||
this.setState({ shiftIdToShowRotationForm: 'new', layerIndexToShowRotationForm: layerIndex });
|
||||
};
|
||||
|
||||
handleAddRotation = (option) => {
|
||||
this.setState({ shiftIdToShowRotationForm: option.value });
|
||||
handleAddRotation = (option: SelectOption) => {
|
||||
this.setState({ shiftIdToShowRotationForm: 'new', layerIndexToShowRotationForm: option.value });
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
|
|||
|
||||
const shifts = store.scheduleStore.events[scheduleId]?.['final']?.[getFromString(startMoment)];
|
||||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
|
|
@ -52,17 +54,17 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
|
|||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={cx('title')}>Final schedule</div>
|
||||
<Input
|
||||
{/*<Input
|
||||
prefix={<Icon name="search" />}
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={this.onSearchTermChangeCallback}
|
||||
/>
|
||||
/>*/}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx('header-plus-content')}>
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks startMoment={startMoment} />
|
||||
<div className={cx('rotations')}>
|
||||
{shifts && shifts.length ? (
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
|
||||
const currentTimeX = diff / base;
|
||||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
|
|
@ -60,7 +62,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks startMoment={startMoment} />
|
||||
<div className={cx('rotations')}>
|
||||
{shifts && shifts.length ? (
|
||||
|
|
@ -89,7 +91,9 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('add-rotations-layer')}>Add override +</div>
|
||||
<div className={cx('add-rotations-layer')} onClick={this.handleAddOverride}>
|
||||
Add override +
|
||||
</div>
|
||||
</div>
|
||||
{shiftIdToShowOverrideForm && (
|
||||
<ScheduleOverrideForm
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { action, observable, toJS } from 'mobx';
|
|||
import ReactCSSTransitionGroup from 'react-transition-group'; // ES6
|
||||
|
||||
import BaseStore from 'models/base_store';
|
||||
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { makeRequest } from 'network';
|
||||
import { RootStore } from 'state';
|
||||
import { SelectOption } from 'state/types';
|
||||
|
||||
import { fillGaps } from './schedule.helpers';
|
||||
import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift } from './schedule.types';
|
||||
import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event } from './schedule.types';
|
||||
|
||||
const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
|
||||
|
||||
|
|
@ -170,20 +171,34 @@ export class ScheduleStore extends BaseStore {
|
|||
|
||||
// ------- NEW SCHEDULES API ENDPOINTS ---------
|
||||
|
||||
async createRotation(scheduleId: Schedule['id'], isOverride: boolean, params: any) {
|
||||
async createRotation(scheduleId: Schedule['id'], isOverride: boolean, params: Partial<Shift>) {
|
||||
const type = isOverride ? 3 : 2;
|
||||
|
||||
return await makeRequest(`/oncall_shifts/`, {
|
||||
const response = await makeRequest(`/oncall_shifts/`, {
|
||||
data: { type, schedule: scheduleId, ...params },
|
||||
method: 'POST',
|
||||
});
|
||||
}).catch(this.onApiError);
|
||||
|
||||
this.shifts = {
|
||||
...this.shifts,
|
||||
[response.id]: response,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async updateRotation(rotationId: Rotation['id']) {
|
||||
return await makeRequest(`/oncall_shifts/`, {
|
||||
params: { shift_id: rotationId },
|
||||
method: 'GET',
|
||||
});
|
||||
async updateRotation(shiftId: Shift['id'], params: Partial<Shift>) {
|
||||
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
|
||||
data: { ...params },
|
||||
method: 'PUT',
|
||||
}).catch(this.onApiError);
|
||||
|
||||
this.shifts = {
|
||||
...this.shifts,
|
||||
[response.id]: response,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async updateRotationMock(rotationId: Rotation['id'], fromString: string, currentTimezone: Timezone) {
|
||||
|
|
@ -256,6 +271,16 @@ export class ScheduleStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
@action
|
||||
async updateOncallShift(shiftId: Shift['id']) {
|
||||
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {});
|
||||
|
||||
this.shifts = {
|
||||
...this.shifts,
|
||||
[shiftId]: response,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteOncallShift(shiftId: Shift['id']) {
|
||||
return await makeRequest(`/oncall_shifts/${shiftId}`, {
|
||||
method: 'DELETE',
|
||||
|
|
@ -287,6 +312,21 @@ export class ScheduleStore extends BaseStore {
|
|||
}
|
||||
}
|
||||
|
||||
shifts.forEach((shift) => {
|
||||
for (let i = 0; i < shift.events.length; i++) {
|
||||
const iEvent = shift.events[i];
|
||||
|
||||
for (let j = i + 1; j < shift.events.length; j++) {
|
||||
const jEvent = shift.events[j];
|
||||
if (iEvent.start === jEvent.start && iEvent.end === jEvent.end) {
|
||||
iEvent.users.push(...jEvent.users);
|
||||
jEvent.merged = true;
|
||||
}
|
||||
}
|
||||
shift.events = shift.events.filter((event) => !event.merged);
|
||||
}
|
||||
});
|
||||
|
||||
shifts.forEach((shift) => {
|
||||
shift.events = fillGaps(shift.events);
|
||||
});
|
||||
|
|
@ -305,16 +345,6 @@ export class ScheduleStore extends BaseStore {
|
|||
};
|
||||
|
||||
console.log(toJS(this.events));
|
||||
|
||||
/*this.rotations = {
|
||||
...this.rotations,
|
||||
[rotationId]: {
|
||||
...this.rotations[rotationId],
|
||||
[level]: {
|
||||
[fromString]: response as Rotation,
|
||||
},
|
||||
},
|
||||
};*/
|
||||
}
|
||||
|
||||
async updateFrequencyOptions() {
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ export interface CreateScheduleExportTokenResponse {
|
|||
}
|
||||
|
||||
export interface Shift {
|
||||
by_day: null;
|
||||
frequency: number;
|
||||
by_day: string[];
|
||||
frequency: number | null;
|
||||
id: string;
|
||||
interval: null;
|
||||
interval: number;
|
||||
priority_level: number;
|
||||
rolling_users: Array<Array<User['pk']>>;
|
||||
rotation_start: string;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
|
||||
import { User, UserRole } from './user.types';
|
||||
|
||||
export const getIconType = (role: UserRole) => {
|
||||
|
|
@ -31,6 +34,23 @@ export const getRole = (role: UserRole) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const getTimezone = (user: User) => {
|
||||
const tzByName = {
|
||||
'Hello Oncall': 'UTC',
|
||||
'Matías Bordese': 'America/Montevideo',
|
||||
'Michael Derynck': 'America/Vancouver',
|
||||
'Yulia Shanyrova': 'Europe/Amsterdam',
|
||||
'Maxim Mordasov': 'Europe/Moscow',
|
||||
'Vadim Stepanov': 'Europe/London',
|
||||
'Ildar Iskhakov': 'Asia/Yerevan',
|
||||
'Raphael Batyrbaev': 'Europe/Rome',
|
||||
'Innokentii Konstantinov': 'Asia/Singapore',
|
||||
'Matvey Kukuy': 'Asia/Tel_Aviv',
|
||||
};
|
||||
|
||||
return user.timezone || tzByName[user.username] || dayjs.tz.guess();
|
||||
};
|
||||
|
||||
export const getUserNotificationsSummary = (user: User) => {
|
||||
if (!user) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { get } from 'lodash-es';
|
||||
import { action, computed, observable } from 'mobx';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import BaseStore from 'models/base_store';
|
||||
import { NotificationPolicyType } from 'models/notification_policy';
|
||||
|
|
@ -9,7 +11,7 @@ import { Mixpanel } from 'services/mixpanel';
|
|||
import { RootStore } from 'state';
|
||||
import { move } from 'state/helpers';
|
||||
|
||||
import { prepareForUpdate } from './user.helpers';
|
||||
import { getTimezone, prepareForUpdate } from './user.helpers';
|
||||
import { User } from './user.types';
|
||||
|
||||
export class UserStore extends BaseStore {
|
||||
|
|
@ -54,7 +56,7 @@ export class UserStore extends BaseStore {
|
|||
|
||||
this.items = {
|
||||
...this.items,
|
||||
[user.pk]: { ...user, timezone: this.rootStore.currentTimezone },
|
||||
[user.pk]: { ...user, timezone: getTimezone(user) },
|
||||
};
|
||||
|
||||
this.currentUserPk = user.pk;
|
||||
|
|
@ -100,27 +102,7 @@ export class UserStore extends BaseStore {
|
|||
...acc,
|
||||
[item.pk]: {
|
||||
...item,
|
||||
timezone: {
|
||||
'Hello Oncall': 'UTC',
|
||||
'Matías Bordese': 'America/Montevideo',
|
||||
'Michael Derynck': 'America/Vancouver',
|
||||
'Yulia Shanyrova': 'Europe/Amsterdam',
|
||||
'Maxim Mordasov': 'Europe/Moscow',
|
||||
'Vadim Stepanov': 'Europe/London',
|
||||
'Ildar Iskhakov': 'Asia/Yerevan',
|
||||
'Raphael Batyrbaev': 'Europe/Rome',
|
||||
'Innokentii Konstantinov': 'Asia/Singapore',
|
||||
/* 'Matvey Kukuy',*/
|
||||
}[item.username],
|
||||
working_hours: {
|
||||
monday: [{ start: '09:00:00', end: '18:00:00' }],
|
||||
tuesday: [{ start: '09:00:00', end: '18:00:00' }],
|
||||
wednesday: [{ start: '09:00:00', end: '18:00:00' }],
|
||||
thursday: [{ start: '09:00:00', end: '18:00:00' }],
|
||||
friday: [{ start: '09:00:00', end: '18:00:00' }],
|
||||
saturday: [],
|
||||
sunday: [],
|
||||
},
|
||||
timezone: getTimezone(item),
|
||||
},
|
||||
}),
|
||||
{}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { DateTime } from '@grafana/data';
|
||||
import { dateTime, DateTime } from '@grafana/data';
|
||||
import dayjs from 'dayjs';
|
||||
import { subtract } from 'lodash-es';
|
||||
|
||||
|
|
@ -678,3 +678,11 @@ export const getUTCString = (moment: dayjs.Dayjs | DateTime, timezone: Timezone)
|
|||
.subtract(timezoneOffset, 'minutes')
|
||||
.format('YYYY-MM-DDTHH:mm:ss.000Z');
|
||||
};
|
||||
|
||||
export const getDateTime = (date: string) => {
|
||||
const browserTimezone = dayjs.tz.guess();
|
||||
|
||||
const browserTimezoneOffset = dayjs().tz(browserTimezone).utcOffset();
|
||||
|
||||
return dateTime(dayjs(date).subtract(browserTimezoneOffset, 'minutes').format('YYYY-MM-DDTHH:mm:ss.000Z'));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AppRootProps } from '@grafana/data';
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Button, HorizontalGroup, VerticalGroup, RadioButtonGroup, IconButton, ToolbarButton } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
|
|
@ -15,6 +16,7 @@ import Text from 'components/Text/Text';
|
|||
// import UsersTimezones from 'components/UsersTimezones/UsersTimezones';
|
||||
import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect';
|
||||
import UsersTimezones from 'components/UsersTimezones/UsersTimezones';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import Rotations from 'containers/Rotations/Rotations';
|
||||
import ScheduleFinal from 'containers/Rotations/ScheduleFinal';
|
||||
import ScheduleOverrides from 'containers/Rotations/ScheduleOverrides';
|
||||
|
|
@ -66,7 +68,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
store.scheduleStore.updateItem(id);
|
||||
store.scheduleStore.updateFrequencyOptions();
|
||||
store.scheduleStore.updateDaysOptions();
|
||||
store.scheduleStore.updateOncallShifts(id);
|
||||
await store.scheduleStore.updateOncallShifts(id); // TODO we should know shifts to render Rotations
|
||||
|
||||
this.updateEvents();
|
||||
}
|
||||
|
|
@ -93,7 +95,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xxl" />
|
||||
</PluginLink>
|
||||
<Text.Title level={3}>{schedule?.name}</Text.Title>
|
||||
<ScheduleCounter
|
||||
{/*<ScheduleCounter
|
||||
type="link"
|
||||
count={5}
|
||||
tooltipTitle="Used in escalations"
|
||||
|
|
@ -112,18 +114,20 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
count={2}
|
||||
tooltipTitle="Warnings"
|
||||
tooltipContent="Schedule has unassigned time periods during next 7 days"
|
||||
/>
|
||||
/>*/}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
{users && (
|
||||
<UserTimezoneSelect value={currentTimezone} users={users} onChange={this.handleTimezoneChange} />
|
||||
)}
|
||||
<ScheduleQuality quality={0.89} />
|
||||
{/*<ScheduleQuality quality={0.89} />
|
||||
<ToolbarButton icon="copy" tooltip="Copy" />
|
||||
<ToolbarButton icon="brackets-curly" tooltip="Code" />
|
||||
<ToolbarButton icon="share-alt" tooltip="Share" />
|
||||
<ToolbarButton icon="cog" tooltip="Settings" />
|
||||
<ToolbarButton icon="trash-alt" tooltip="Delete" />
|
||||
<ToolbarButton icon="cog" tooltip="Settings" />*/}
|
||||
<WithConfirm>
|
||||
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
|
|
@ -150,7 +154,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup width="auto">
|
||||
{/*<HorizontalGroup width="auto">
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: 'Day', value: 'day' },
|
||||
|
|
@ -175,7 +179,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
value={renderType}
|
||||
onChange={this.handleRenderTypeChange}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>*/}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
{/* <div className={'current-time'} />*/}
|
||||
|
|
@ -257,6 +261,17 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
this.setState({ startMoment: startMoment.add(-7, 'day') }, this.updateEvents);
|
||||
};
|
||||
|
||||
handleDelete = () => {
|
||||
const {
|
||||
store,
|
||||
query: { id: scheduleId },
|
||||
} = this.props;
|
||||
|
||||
store.scheduleStore.delete(scheduleId).then(() => {
|
||||
getLocationSrv().update({ query: { page: 'schedules' } });
|
||||
});
|
||||
};
|
||||
|
||||
handleRightClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/*
|
||||
.root .expanded-row {
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import cn from 'classnames/bind';
|
|||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import NewScheduleSelector from 'components/NewScheduleSelector/NewScheduleSelector';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import ScheduleCounter from 'components/ScheduleCounter/ScheduleCounter';
|
||||
|
|
@ -69,25 +70,25 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
const schedules = scheduleStore.getSearchResult();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
/* {
|
||||
width: '10%',
|
||||
title: 'Status',
|
||||
key: 'name',
|
||||
render: this.renderStatus,
|
||||
},
|
||||
},*/
|
||||
{
|
||||
width: '30%',
|
||||
width: '50%',
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
render: this.renderName,
|
||||
},
|
||||
{
|
||||
width: '30%',
|
||||
width: '45%',
|
||||
title: 'OnCall',
|
||||
key: 'users',
|
||||
render: this.renderUsers,
|
||||
render: this.renderOncallNow,
|
||||
},
|
||||
{
|
||||
/*{
|
||||
width: '20%',
|
||||
title: 'ChatOps',
|
||||
key: 'chatops',
|
||||
|
|
@ -98,8 +99,9 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
title: 'Quality',
|
||||
key: 'quality',
|
||||
render: this.renderQuality,
|
||||
},
|
||||
},*/
|
||||
{
|
||||
width: '5%',
|
||||
key: 'buttons',
|
||||
render: this.renderButtons,
|
||||
},
|
||||
|
|
@ -234,16 +236,20 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
return <PluginLink query={{ page: 'schedule', id: item.id }}>{item.name}</PluginLink>;
|
||||
};
|
||||
|
||||
renderUsers = (item: Schedule) => {
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
{/*{item.users.map((user) => (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Avatar src={user.avatar} size="large" /> {user.name}
|
||||
</HorizontalGroup>
|
||||
))}*/}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
renderOncallNow = (item: Schedule, index: number) => {
|
||||
if (item.on_call_now?.length > 0) {
|
||||
return item.on_call_now.map((user, index) => {
|
||||
return (
|
||||
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }}>
|
||||
<div>
|
||||
<Avatar size="small" src={user.avatar} />
|
||||
<Text type="secondary"> {user.username}</Text>
|
||||
</div>
|
||||
</PluginLink>
|
||||
);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderChatOps = (item: Schedule) => {
|
||||
|
|
@ -259,9 +265,9 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
renderButtons = (item: Schedule) => {
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<IconButton tooltip="Copy" name="copy" />
|
||||
{/*<IconButton tooltip="Copy" name="copy" />
|
||||
<IconButton tooltip="Settings" name="cog" />
|
||||
<IconButton tooltip="Code" name="brackets-curly" />
|
||||
<IconButton tooltip="Code" name="brackets-curly" />*/}
|
||||
<WithConfirm>
|
||||
<IconButton tooltip="Delete" name="trash-alt" onClick={this.getDeleteScheduleClickHandler(item.id)} />
|
||||
</WithConfirm>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue