diff --git a/grafana-plugin/src/GrafanaPluginRootPage.tsx b/grafana-plugin/src/GrafanaPluginRootPage.tsx index ee56ed40..4863734c 100644 --- a/grafana-plugin/src/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/GrafanaPluginRootPage.tsx @@ -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'; diff --git a/grafana-plugin/src/components/Rotations/Rotations.helpers.ts b/grafana-plugin/src/components/Rotations/Rotations.helpers.ts index 92f470cc..f83efb50 100644 --- a/grafana-plugin/src/components/Rotations/Rotations.helpers.ts +++ b/grafana-plugin/src/components/Rotations/Rotations.helpers.ts @@ -16,7 +16,7 @@ export const getRandomTimeslots = (count = 6, layerIndex, rotationIndex) => { start, end, inactive, - users: [getRandomUser(), getRandomUser()], + users: [getRandomUser() /*, getRandomUser()*/], color: getColor(layerIndex, rotationIndex), }); } diff --git a/grafana-plugin/src/components/Rotations/Rotations.tsx b/grafana-plugin/src/components/Rotations/Rotations.tsx index eda572d3..0c47cef9 100644 --- a/grafana-plugin/src/components/Rotations/Rotations.tsx +++ b/grafana-plugin/src/components/Rotations/Rotations.tsx @@ -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 {
{rotations.map((rotation, rotationIndex) => ( - = (props) => { - const { color, user, inactive, label } = props; +const ScheduleSlot: FC = 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 ( - }> + }>
@@ -51,13 +56,13 @@ const ScheduleSlot: FC = (props) => {
); -}; +}); 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, })} /> - {user} + {user?.username} @@ -102,3 +107,9 @@ const ScheduleSlotDetails = (props) => {
); }; + +interface ScheduleGapProps {} + +export const ScheduleGap = (props: ScheduleGapProps) => { + return
; +}; diff --git a/grafana-plugin/src/components/ScheduleTimeline/ScheduleTimeline.tsx b/grafana-plugin/src/components/ScheduleTimeline/ScheduleTimeline.tsx deleted file mode 100644 index 45d872de..00000000 --- a/grafana-plugin/src/components/ScheduleTimeline/ScheduleTimeline.tsx +++ /dev/null @@ -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 = (props) => { - const { layerIndex, rotationIndex, slots, label } = props; - - return ( -
- {/*
*/} -
-
- {slots.map(({ users, inactive, color }, slotIndex) => { - return ( -
- {users.map((user, userIndex) => ( - - ))} -
- ); - })} -
-
-
- ); -}; - -export default ScheduleTimeline; diff --git a/grafana-plugin/src/components/ScheduleTimeline/ScheduleTimeline.module.css b/grafana-plugin/src/containers/Rotation/Rotation.module.css similarity index 93% rename from grafana-plugin/src/components/ScheduleTimeline/ScheduleTimeline.module.css rename to grafana-plugin/src/containers/Rotation/Rotation.module.css index 261807fc..dfe0219a 100644 --- a/grafana-plugin/src/components/ScheduleTimeline/ScheduleTimeline.module.css +++ b/grafana-plugin/src/containers/Rotation/Rotation.module.css @@ -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; diff --git a/grafana-plugin/src/containers/Rotation/Rotation.tsx b/grafana-plugin/src/containers/Rotation/Rotation.tsx new file mode 100644 index 00000000..370a91b6 --- /dev/null +++ b/grafana-plugin/src/containers/Rotation/Rotation.tsx @@ -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 = 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 ; + } + + const { shifts } = rotation; + + return ( +
+ {/*
*/} +
+
+ {shifts.map(({ start, duration, users }, slotIndex) => { + const inactive = false; + + const width = duration / (60 * 60 * 24 * 7); + + return ( +
+ {users.length ? ( + users.map((pk, userIndex) => { + return ( + + ); + }) + ) : ( + + )} +
+ ); + })} +
+
+
+ ); +}); + +export default Rotation; diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index 14d02744..adc66049 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -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, + }; + } } diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index 800df6d2..0c9ffe09 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -43,3 +43,14 @@ export interface CreateScheduleExportTokenResponse { created_at: string; export_url: string; } + +export interface Shift { + start: string; + duration: number; // in seconds + users: Array; +} + +export interface Rotation { + id: string; + shifts: Shift[]; +} diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 30fa1744..5227d54e 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -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 tz: 'Europe/Moscow', }; - async componentDidMount() {} + async componentDidMount() { + const { store } = this.props; + + store.userStore.updateItems(); + } componentDidUpdate() {} diff --git a/grafana-plugin/src/pages/schedules/Schedules.module.css b/grafana-plugin/src/pages/schedules/Schedules.module.css index 12747b4a..855f3b38 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.module.css +++ b/grafana-plugin/src/pages/schedules/Schedules.module.css @@ -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; -} diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index d07b39ea..7ae5647b 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -479,7 +479,7 @@ const Event = ({ event }: EventProps) => { return ( {!event.is_gap ? ( - <> +
{`L${event.priority_level || '0'}`}
@@ -504,10 +504,11 @@ const Event = ({ event }: EventProps) => { {dates}
- + ) : (
- Gap! Nobody On-Call... + Gap! Nobody + On-Call...
)} diff --git a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx index 0f2ac804..4c636202 100644 --- a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx @@ -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
- +
);