split rotation into layers

This commit is contained in:
Maxim 2022-07-29 18:58:27 +03:00
parent bece1dab38
commit 949b2d7618
17 changed files with 339 additions and 175 deletions

View file

@ -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]
);

View file

@ -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">

View file

@ -24,8 +24,7 @@
flex-direction: column;
gap: 5px;
padding-bottom: 8px;
/* overflow: hidden; */
overflow: hidden;
}
.root:first-child .timeline {

View file

@ -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[];

View file

@ -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>

View file

@ -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);
}

View file

@ -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 });
};
}

View file

@ -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 ? (

View file

@ -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

View file

@ -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() {

View file

@ -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;

View file

@ -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;

View file

@ -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),
},
}),
{}

View file

@ -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'));
};

View file

@ -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;

View file

@ -11,6 +11,8 @@
margin: 20px 0;
}
/*
.root .expanded-row {
background: var(--secondary-background);
}
*/

View file

@ -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>