add fake rotation data

This commit is contained in:
Maxim 2022-07-04 10:08:44 +01:00
parent 4ed223ad6a
commit b3986f652e
14 changed files with 226 additions and 97 deletions

View file

@ -3,11 +3,9 @@ import React, { useEffect, useMemo } from 'react';
import { AppRootProps } from '@grafana/data';
import { Button, HorizontalGroup, LinkButton, VerticalGroup } from '@grafana/ui';
import dayjs from 'dayjs';
dayjs.extend(utc);
dayjs.extend(timezone);
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import weekday from 'dayjs/plugin/weekday';
import { observer, Provider } from 'mobx-react';
import 'interceptors';
@ -20,6 +18,12 @@ import { rootStore } from 'state';
import { useStore } from 'state/useStore';
import { useNavModel } from 'utils/hooks';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(weekday);
// dayjs().weekday(0);
import './vars.css';
import './index.css';

View file

@ -16,7 +16,7 @@ export const getRandomTimeslots = (count = 6, layerIndex, rotationIndex) => {
start,
end,
inactive,
users: [getRandomUser(), getRandomUser()],
users: [getRandomUser() /*, getRandomUser()*/],
color: getColor(layerIndex, rotationIndex),
});
}

View file

@ -5,10 +5,9 @@ import cn from 'classnames/bind';
import * as dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import Rotation from 'components/Rotation/Rotation';
import RotationForm from 'components/RotationForm/RotationForm';
import ScheduleTimeline from 'components/ScheduleTimeline/ScheduleTimeline';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
import { getColor, getLabel, getRandomTimeslots, getRandomUser } from './Rotations.helpers';
@ -79,7 +78,8 @@ class Rotations extends Component<RotationsProps, RotationsState> {
<TimelineMarks startMoment={startMoment} />
<div className={cx('rotations')}>
{rotations.map((rotation, rotationIndex) => (
<ScheduleTimeline
<Rotation
id={`${layerIndex}-${rotationIndex}`}
layerIndex={layerIndex}
rotationIndex={rotationIndex}
slots={getRandomTimeslots(6, layerIndex, rotationIndex)}

View file

@ -7,6 +7,11 @@
gap: 4px;
}
.root__type_gap {
background: rgba(209, 14, 92, 0.2);
border: 1px dashed #FF5286;
}
.root__inactive {
opacity: 0.5;
}
@ -26,7 +31,7 @@
padding: 2px 4px;
margin: 4px;
line-height: 16px;
z-index: 1;
z-index: 1;
}
.striped {
@ -45,16 +50,16 @@
);
}
.details{
.details {
width: auto;
}
.details-user-status{
.details-user-status {
width: 10px;
height: 10px;
border-radius: 50%;
}
.details-user-status__type_success{
.details-user-status__type_success {
background-color: var(--success-text-color);
}

View file

@ -2,43 +2,48 @@ import React, { FC } from 'react';
import { HorizontalGroup, VerticalGroup, Icon, Tooltip, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import Line from 'components/ScheduleUserDetails/img/line.svg';
import Text from 'components/Text/Text';
import { User } from 'models/user/user.types';
import { useStore } from 'state/useStore';
import styles from './ScheduleSlot.module.css';
interface ScheduleSlotProps {
color: string;
user: string;
userPk: User['pk'];
label: string;
inactive: boolean;
width: number;
}
const cx = cn.bind(styles);
const ScheduleSlot: FC<ScheduleSlotProps> = (props) => {
const { color, user, inactive, label } = props;
const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
const { color, userPk, inactive, label } = props;
const left = Math.random() * 50;
const right = 100 - (left + 20 + Math.random() * 30);
const width = Math.random() * 150 + 100;
const store = useStore();
let title = user;
if (width < 150) {
title = title
.split(' ')
.map((word) => word.charAt(0).toUpperCase())
.join('');
}
const storeUser = store.userStore.items[userPk];
let title = storeUser
? storeUser.username
.split(' ')
.map((word) => word.charAt(0).toUpperCase())
.join('')
: null;
return (
<Tooltip content={<ScheduleSlotDetails user={user} />}>
<Tooltip content={<ScheduleSlotDetails user={storeUser} />}>
<div
className={cx('root', { root__inactive: inactive })}
style={{
backgroundColor: color,
width: `${width}px`,
}}
>
<div style={{ left: `${left}%`, right: `${right}%` }} className={cx('striped')} />
@ -51,13 +56,13 @@ const ScheduleSlot: FC<ScheduleSlotProps> = (props) => {
</div>
</Tooltip>
);
};
});
export default ScheduleSlot;
interface ScheduleSlotDetailsProps {}
const ScheduleSlotDetails = (props) => {
const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
const { user, currentUser } = props;
const userStatus = 'success';
@ -72,7 +77,7 @@ const ScheduleSlotDetails = (props) => {
[`details-user-status__type_${userStatus}`]: true,
})}
/>
<Text type="secondary">{user}</Text>
<Text type="secondary">{user?.username}</Text>
</HorizontalGroup>
<HorizontalGroup>
<VerticalGroup spacing="none">
@ -102,3 +107,9 @@ const ScheduleSlotDetails = (props) => {
</div>
);
};
interface ScheduleGapProps {}
export const ScheduleGap = (props: ScheduleGapProps) => {
return <div className={cx('root', 'root__type_gap')} style={{}} />;
};

View file

@ -1,49 +0,0 @@
import React, { FC, useMemo, useState } from 'react';
import cn from 'classnames/bind';
import * as dayjs from 'dayjs';
import ScheduleSlot from 'components/ScheduleSlot/ScheduleSlot';
import Text from 'components/Text/Text';
import styles from './ScheduleTimeline.module.css';
const cx = cn.bind(styles);
interface ScheduleSlotState {}
interface ScheduleTimelineProps {
layerIndex: number;
rotationIndex: number;
}
const ScheduleTimeline: FC<ScheduleTimelineProps> = (props) => {
const { layerIndex, rotationIndex, slots, label } = props;
return (
<div className={cx('root')}>
{/* <div className={cx('current-time')} />*/}
<div className={cx('timeline')}>
<div className={cx('slots')}>
{slots.map(({ users, inactive, color }, slotIndex) => {
return (
<div className={cx('users')}>
{users.map((user, userIndex) => (
<ScheduleSlot
key={userIndex}
color={color}
label={slotIndex === 0 && userIndex === 0 && label}
user={user}
inactive={inactive}
/>
))}
</div>
);
})}
</div>
</div>
</div>
);
};
export default ScheduleTimeline;

View file

@ -27,16 +27,18 @@
.slots {
display: flex;
gap: 4px;
padding: 0 2px;
}
.users{
.stack {
display: flex;
flex-direction: column;
gap:1px;
}
.stack > div {
margin: 0 2px;
}
.current-time {
position: absolute;
left: 450px;

View file

@ -0,0 +1,80 @@
import React, { FC, useMemo, useState, useEffect } from 'react';
import { LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
import * as dayjs from 'dayjs';
import { observer } from 'mobx-react';
import { getColor } from 'components/Rotations/Rotations.helpers';
import ScheduleSlot, { ScheduleGap } from 'components/ScheduleSlot/ScheduleSlot';
import Text from 'components/Text/Text';
import { Rotation as RotationType } from 'models/schedule/schedule.types';
import { useStore } from 'state/useStore';
import styles from './Rotation.module.css';
const cx = cn.bind(styles);
interface ScheduleSlotState {}
interface RotationProps {
id: RotationType['id'];
layerIndex: number;
rotationIndex: number;
label: string;
}
const Rotation: FC<RotationProps> = observer((props) => {
const { id, layerIndex, rotationIndex, label } = props;
const store = useStore();
useEffect(() => {
store.scheduleStore.updateRotation(id);
}, []);
const rotation = store.scheduleStore.rotations[id];
if (!rotation) {
return <LoadingPlaceholder text="Loading shifts..." />;
}
const { shifts } = rotation;
return (
<div className={cx('root')}>
{/* <div className={cx('current-time')} />*/}
<div className={cx('timeline')}>
<div className={cx('slots')}>
{shifts.map(({ start, duration, users }, slotIndex) => {
const inactive = false;
const width = duration / (60 * 60 * 24 * 7);
return (
<div className={cx('stack')} style={{ width: `${width * 100}%` }}>
{users.length ? (
users.map((pk, userIndex) => {
return (
<ScheduleSlot
key={userIndex}
color={getColor(layerIndex, rotationIndex)}
label={slotIndex === 0 && userIndex === 0 && label}
userPk={pk}
inactive={inactive}
/>
);
})
) : (
<ScheduleGap />
)}
</div>
);
})}
</div>
</div>
</div>
);
});
export default Rotation;

View file

@ -1,11 +1,14 @@
import { omit } from 'lodash-es';
import dayjs from 'dayjs';
import { omit, reject } from 'lodash-es';
import { action, observable, toJS } from 'mobx';
import BaseStore from 'models/base_store';
import { makeRequest } from 'network';
import { RootStore } from 'state';
import { Schedule, ScheduleEvent } from './schedule.types';
import { Rotation, Schedule, ScheduleEvent } from './schedule.types';
const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
export class ScheduleStore extends BaseStore {
@observable
@ -14,6 +17,9 @@ export class ScheduleStore extends BaseStore {
@observable.shallow
items: { [id: string]: Schedule } = {};
@observable.shallow
rotations: { [id: string]: Rotation } = {};
@observable
scheduleToScheduleEvents: {
[id: string]: ScheduleEvent[];
@ -107,4 +113,57 @@ export class ScheduleStore extends BaseStore {
method: 'DELETE',
});
}
async updateRotation(rotationId: Rotation['id'], from?: string) {
const response = await new Promise((resolve, reject) => {
function getUsers() {
const rnd = Math.random();
if (rnd > 0.66) {
return [];
}
const users = [
'UCXTPJYKQHFW6',
'UFYP8IJV9BZDE',
'U122EFECQFN9Y',
'UZ2LWBDAZE962',
'U87ZI7PRWF7K1',
'U2VY9ZP5A1XKL',
'UTA6SS7RL3HC7',
'UAYAYSDVG5MYH',
];
if (rnd > 0.33) {
return [users[Math.floor(Math.random() * users.length)]];
}
return [users[Math.floor(Math.random() * users.length)], users[Math.floor(Math.random() * users.length)]];
}
setTimeout(() => {
if (!from) {
from = dayjs().startOf('week').format('YYYY-MM-DDTHH:mm:ss');
}
const startMoment = dayjs(`${from}.000Z`).utc();
const shifts = [];
for (let i = 0; i < 14; i++) {
shifts.push({
start: dayjs(startMoment).add(3 * i, 'hour'),
duration: (Math.floor(Math.random() * 6) + 8) * 60 * 60,
users: getUsers(),
});
}
resolve({ id: rotationId, shifts });
}, 500);
});
this.rotations = {
...this.rotations,
[rotationId]: response as Rotation,
};
}
}

View file

@ -43,3 +43,14 @@ export interface CreateScheduleExportTokenResponse {
created_at: string;
export_url: string;
}
export interface Shift {
start: string;
duration: number; // in seconds
users: Array<User['pk']>;
}
export interface Rotation {
id: string;
shifts: Shift[];
}

View file

@ -18,6 +18,7 @@ import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect
import UsersTimezones from 'components/UsersTimezones/UsersTimezones';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import { getRandomUsers } from './Schedule.helpers';
@ -26,7 +27,7 @@ import styles from './Schedule.module.css';
const cx = cn.bind(styles);
interface SchedulePageProps extends AppRootProps {}
interface SchedulePageProps extends AppRootProps, WithStoreProps {}
interface SchedulePageState {
startMoment: dayjs.Dayjs;
@ -46,7 +47,11 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
tz: 'Europe/Moscow',
};
async componentDidMount() {}
async componentDidMount() {
const { store } = this.props;
store.userStore.updateItems();
}
componentDidUpdate() {}

View file

@ -54,19 +54,15 @@
text-align: center;
font-size: 14px;
font-weight: 500;
flex-shrink: 0;
}
.gap-between-shifts {
width: 520px;
height: 32px;
padding: 4px 4px 24px 4px;
padding: 5px 5px 5px 24px;
background-color: rgba(209, 14, 92, 0.15);
border: 1px solid rgba(209, 14, 92, 0.15);
border-radius: 50px;
color: #ff5286;
font-weight: 400;
}
.gap-between-shifts-icon {
margin-left: 24px;
}

View file

@ -479,7 +479,7 @@ const Event = ({ event }: EventProps) => {
return (
<HorizontalGroup align="flex-start" spacing="md">
{!event.is_gap ? (
<>
<HorizontalGroup align="flex-start">
<div className={cx('priority-icon')}>
<Text type="secondary">{`L${event.priority_level || '0'}`}</Text>
</div>
@ -504,10 +504,11 @@ const Event = ({ event }: EventProps) => {
<Text type="secondary"> {dates}</Text>
</div>
</VerticalGroup>
</>
</HorizontalGroup>
) : (
<div className={cx('gap-between-shifts')}>
<Icon name="exclamation-triangle" className={cx('gap-between-shifts-icon')} /> Gap! Nobody On-Call...
<Icon size="sm" name="exclamation-triangle" className={cx('gap-between-shifts-icon')} /> Gap! Nobody
On-Call...
</div>
)}
</HorizontalGroup>

View file

@ -7,19 +7,19 @@ import { observer } from 'mobx-react';
import Avatar from 'components/Avatar/Avatar';
import PluginLink from 'components/PluginLink/PluginLink';
import Rotation from 'components/Rotation/Rotation';
import { getColor, getLabel } from 'components/Rotations/Rotations.helpers';
import ScheduleCounter from 'components/ScheduleCounter/ScheduleCounter';
import ScheduleTimeline from 'components/ScheduleTimeline/ScheduleTimeline';
import SchedulesFilters from 'components/SchedulesFilters_NEW/SchedulesFilters';
import { SchedulesFiltersType } from 'components/SchedulesFilters_NEW/SchedulesFilters.types';
import Table from 'components/Table/Table';
import Text from 'components/Text/Text';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import GSelect from 'containers/GSelect/GSelect';
import Rotation from 'containers/Rotation/Rotation';
import { Schedule } from 'models/schedule/schedule.types';
import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config';
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import { getRandomSchedules, getRandomTimeslots } from './Schedules.helpers';
@ -28,7 +28,7 @@ import styles from './Schedules.module.css';
const cx = cn.bind(styles);
interface SchedulesPageProps {}
interface SchedulesPageProps extends WithStoreProps {}
interface SchedulesPageState {
startMoment: dayjs.Dayjs;
@ -43,7 +43,11 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
filters: { searchTerm: '', status: 'all', type: 'all' },
};
async componentDidMount() {}
async componentDidMount() {
const { store } = this.props;
store.userStore.updateItems();
}
componentDidUpdate() {}
@ -127,7 +131,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
<div className={cx('schedule')}>
<TimelineMarks startMoment={startMoment} />
<div className={cx('rotations')}>
<ScheduleTimeline layerIndex={1} rotationIndex={2} slots={getRandomTimeslots()} />
<Rotation id={`${1}-${2}`} layerIndex={1} rotationIndex={2} slots={getRandomTimeslots()} />
</div>
</div>
);