add fake rotation data
This commit is contained in:
parent
4ed223ad6a
commit
b3986f652e
14 changed files with 226 additions and 97 deletions
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const getRandomTimeslots = (count = 6, layerIndex, rotationIndex) => {
|
|||
start,
|
||||
end,
|
||||
inactive,
|
||||
users: [getRandomUser(), getRandomUser()],
|
||||
users: [getRandomUser() /*, getRandomUser()*/],
|
||||
color: getColor(layerIndex, rotationIndex),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={{}} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
80
grafana-plugin/src/containers/Rotation/Rotation.tsx
Normal file
80
grafana-plugin/src/containers/Rotation/Rotation.tsx
Normal 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;
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue