From 874be7fc9008593808fe40a2413bfeba034f81ff Mon Sep 17 00:00:00 2001 From: Maxim Date: Fri, 19 Aug 2022 15:12:22 +0300 Subject: [PATCH] add color coding --- .../ScheduleSlot/ScheduleSlot.helpers.ts | 41 ------- .../ScheduleSlot/ScheduleSlot.module.css | 2 +- .../components/ScheduleSlot/ScheduleSlot.tsx | 3 +- .../containers/RotationForm/RotationForm.tsx | 2 +- .../src/containers/Rotations/Rotations.tsx | 56 ++------- .../containers/Rotations/ScheduleFinal.tsx | 35 +++++- .../Rotations/ScheduleOverrides.tsx | 9 +- .../src/models/schedule/schedule.helpers.ts | 115 +++++++++++++++++- .../src/models/schedule/schedule.ts | 50 +++----- .../src/models/schedule/schedule.types.ts | 8 +- 10 files changed, 175 insertions(+), 146 deletions(-) diff --git a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.helpers.ts b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.helpers.ts index 3cc508f3..5afee5c7 100644 --- a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.helpers.ts +++ b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.helpers.ts @@ -3,47 +3,6 @@ import dayjs from 'dayjs'; import { Shift } from 'models/schedule/schedule.types'; import { User } from 'models/user/user.types'; -export const getRandomTimeslots = (count = 6, layerIndex, rotationIndex) => { - const slots = []; - for (let i = 0; i < count; i++) { - const start = dayjs() - .startOf('day') - .add(i * 4, 'hour'); - const end = dayjs() - .startOf('day') - .add(i * 4 + 2, 'hour'); - //const inactive = end.isBefore(dayjs()); - const inactive = false; - - slots.push({ - start, - end, - inactive, - users: [getRandomUser() /*, getRandomUser()*/], - color: getColor(layerIndex, rotationIndex), - }); - } - return slots; -}; - -const L1_COLORS = ['#3D71D9', '#1A6BE8', '#6D609C', '#50639C', '#8214A0', '#44449F', '#4D3B72', '#273C6C']; - -const L2_COLORS = ['#3CB979', '#A49E7C', '#188343', '#746D46', '#84362A', '#464121', '#521913', '#414130']; - -const L3_COLORS = ['#377277', '#797B83', '#638282', '#626779', '#364E4E', '#47494F', '#423220', '#44321D']; - -const OVERRIDE_COLORS = ['#C69B06', '#797B83', '#638282', '#626779']; - -export const getOverrideColor = (index: number) => { - return OVERRIDE_COLORS[index]; -}; - -const COLORS = [L1_COLORS, L2_COLORS, L3_COLORS, OVERRIDE_COLORS]; - -export const getColor = (layerIndex: number, rotationIndex: number) => { - return COLORS[layerIndex]?.[rotationIndex]; -}; - const USERS = [ 'Innokentii Konstantinov', 'Ildar Iskhakov', diff --git a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.module.css b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.module.css index 779b263f..99f592d1 100644 --- a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.module.css +++ b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.module.css @@ -1,6 +1,6 @@ .root { height: 28px; - background: #3274d9; + background: #595959; border-radius: 2px; position: relative; display: flex; diff --git a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx index 787d7fa5..d799c53e 100644 --- a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx +++ b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx @@ -30,7 +30,7 @@ interface ScheduleSlotProps { const cx = cn.bind(styles); const ScheduleSlot: FC = observer((props) => { - const { index, layerIndex, rotationIndex, event, startMoment, currentTimezone, color: propColor } = props; + const { index, layerIndex, rotationIndex, event, startMoment, currentTimezone, color } = props; const { users } = event; const trackMouse = false; @@ -62,7 +62,6 @@ const ScheduleSlot: FC = observer((props) => { const inactive = false; - const color = propColor || getColor(layerIndex, rotationIndex); const title = getTitle(storeUser); return ( diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index df652fe0..daa8d552 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx @@ -195,7 +195,7 @@ const RotationForm: FC = observer((props) => { [L{shiftId === 'new' ? layerPriority : shift?.priority_level}] - {shiftId === 'new' ? 'New Rotation' : shift?.title} + {shiftId === 'new' ? 'New Rotation' : shift?.id} diff --git a/grafana-plugin/src/containers/Rotations/Rotations.tsx b/grafana-plugin/src/containers/Rotations/Rotations.tsx index 131ea98e..6fafbf52 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.tsx +++ b/grafana-plugin/src/containers/Rotations/Rotations.tsx @@ -11,14 +11,12 @@ import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; 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 { getColor, getFromString } from 'models/schedule/schedule.helpers'; import { Event, Layer, Schedule, Shift } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { SelectOption, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; -import { getColor, getLabel, getRandomTimeslots, getRandomUser } from './Rotations.helpers'; - import styles from './Rotations.module.css'; const cx = cn.bind(styles); @@ -54,50 +52,9 @@ class Rotations extends Component { const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1; - const storeLayers = store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)] as Layer[]; - - console.log('store.scheduleStore.rotationPreview', store.scheduleStore.rotationPreview); - - let layers = storeLayers; - if (store.scheduleStore.rotationPreview) { - layers = [...layers]; - - const isNew = store.scheduleStore.rotationPreview.shifts[0].shiftId === 'new'; - const priority = store.scheduleStore.rotationPreview.priority; - - let added = false; - layers = layers.reduce((memo, layer, index) => { - if (isNew) { - if (layer.priority === priority) { - const newLayer = { ...layer }; - newLayer.shifts = [...layer.shifts, ...store.scheduleStore.rotationPreview.shifts]; - - memo[index] = newLayer; - - added = true; - } - } else { - const oldShiftIndex = layer.shifts.findIndex( - (shift) => shift.shiftId === store.scheduleStore.rotationPreview.shifts[0].shiftId - ); - if (oldShiftIndex > -1) { - const newLayer = { ...layer }; - newLayer.shifts = [...layer.shifts]; - newLayer.shifts[oldShiftIndex] = store.scheduleStore.rotationPreview.shifts[0]; - - memo[index] = newLayer; - - added = true; - } - } - - return layers; - }, layers); - - if (!added) { - layers.push(store.scheduleStore.rotationPreview); - } - } + const layers = store.scheduleStore.rotationPreview + ? store.scheduleStore.rotationPreview + : (store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)] as Layer[]); const options = layers ? layers.map((layer) => ({ @@ -127,7 +84,7 @@ class Rotations extends Component {
{layers && layers.length ? ( - layers.map((layer) => ( + layers.map((layer, layerIndex) => (
@@ -146,8 +103,9 @@ class Rotations extends Component { onClick={() => { this.onRotationClick(shiftId); }} + color={getColor(layerIndex, rotationIndex)} events={events} - layerIndex={layer.priority - 1} + layerIndex={layerIndex} rotationIndex={rotationIndex} startMoment={startMoment} currentTimezone={currentTimezone} diff --git a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx index b680c2eb..1df5c54f 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx @@ -3,12 +3,13 @@ import React, { Component, useEffect } from 'react'; import { Button, HorizontalGroup, Icon, Input, ValuePicker } from '@grafana/ui'; import cn from 'classnames/bind'; import dayjs from 'dayjs'; +import { toJS } from 'mobx'; import { observer } from 'mobx-react'; import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; import Rotation from 'containers/Rotation/Rotation'; -import { getFromString } from 'models/schedule/schedule.helpers'; -import { Schedule } from 'models/schedule/schedule.types'; +import { getColor, getFromString } from 'models/schedule/schedule.helpers'; +import { Layer, Schedule } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; @@ -47,8 +48,15 @@ class ScheduleFinal extends Component 1; + console.log(toJS(shifts)); + console.log(toJS(layers)); + return ( <>
@@ -70,9 +78,26 @@ class ScheduleFinal extends Component
{shifts && shifts.length ? ( - shifts.map(({ shiftId, events }, index) => ( - - )) + shifts.map(({ shiftId, events }, index) => { + const layerIndex = layers + ? layers.findIndex((layer) => layer.shifts.some((shift) => shift.shiftId === shiftId)) + : -1; + + const rotationIndex = + layerIndex > -1 ? layers[layerIndex].shifts.findIndex((shift) => shift.shiftId === shiftId) : -1; + + console.log(layerIndex, rotationIndex); + + return ( + + ); + }) ) : ( )} diff --git a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx index 37ce474c..16456048 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx @@ -9,7 +9,7 @@ import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; import Rotation from 'containers/Rotation/Rotation'; import { RotationCreateData } from 'containers/RotationForm/RotationForm.types'; import ScheduleOverrideForm from 'containers/RotationForm/ScheduleOverrideForm'; -import { getFromString } from 'models/schedule/schedule.helpers'; +import { getFromString, getOverrideColor } from 'models/schedule/schedule.helpers'; import { Schedule, Shift } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { WithStoreProps } from 'state/types'; @@ -66,11 +66,11 @@ class ScheduleOverrides extends Component
{shifts && shifts.length ? ( - shifts.map(({ shiftId, events }, index) => ( + shifts.map(({ shiftId, events }, rotationIndex) => ( { @@ -81,7 +81,6 @@ class ScheduleOverrides extends Component { diff --git a/grafana-plugin/src/models/schedule/schedule.helpers.ts b/grafana-plugin/src/models/schedule/schedule.helpers.ts index 963a4057..54df9f54 100644 --- a/grafana-plugin/src/models/schedule/schedule.helpers.ts +++ b/grafana-plugin/src/models/schedule/schedule.helpers.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs'; -import { Event, Shift } from './schedule.types'; +import { Event, Layer, ScheduleType, Shift } from './schedule.types'; export const getFromString = (moment: dayjs.Dayjs) => { return moment.format('YYYY-MM-DD'); @@ -16,7 +16,19 @@ export const fillGaps = (events: Event[]) => { if (nextEvent) { if (nextEvent.start !== event.end) { - newEvents.push({ start: event.end, end: nextEvent.start, is_gap: true }); + newEvents.push({ + start: event.end, + end: nextEvent.start, + is_gap: true, + users: [], + all_day: false, + shift: null, + missing_users: [], + is_empty: true, + calendar_type: ScheduleType.API, + priority_level: null, + source: 'web', + }); } } } @@ -25,13 +37,13 @@ export const fillGaps = (events: Event[]) => { }; export const splitToShiftsAndFillGaps = (events: Event[]) => { - const shifts: Array<{ shiftId: Shift['id']; events: Event[] }> = []; + const shifts: Array<{ shiftId: Shift['id']; priority: Shift['priority_level']; events: Event[] }> = []; for (const [i, event] of events.entries()) { if (event.shift?.pk) { let shift = shifts.find((shift) => shift.shiftId === event.shift?.pk); if (!shift) { - shift = { shiftId: event.shift.pk, events: [] }; + shift = { shiftId: event.shift.pk, priority: event.priority_level, events: [] }; shifts.push(shift); } shift.events.push(event); @@ -44,3 +56,98 @@ export const splitToShiftsAndFillGaps = (events: Event[]) => { return shifts; }; + +export const splitToLayers = ( + shifts: Array<{ shiftId: Shift['id']; priority: Shift['priority_level']; events: Event[] }> +) => { + return shifts + .reduce((memo, shift) => { + let layer = memo.find((level) => level.priority === shift.priority); + if (!layer) { + layer = { priority: shift.priority, 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; + }); +}; + +export const enrichLayers = ( + layers: Layer[], + newEvents: Event[], + shiftId: Shift['id'] | 'new', + priority: Shift['priority_level'] +) => { + const updatingLayer = { + priority, + shifts: [{ shiftId: shiftId, events: fillGaps(newEvents.filter((event: Event) => !event.is_gap)) }], + }; + + const isNew = updatingLayer.shifts[0].shiftId === 'new'; + + let added = false; + layers = layers.reduce((memo, layer, index) => { + if (isNew) { + if (layer.priority === priority) { + const newLayer = { ...layer }; + newLayer.shifts = [...layer.shifts, ...updatingLayer.shifts]; + + memo[index] = newLayer; + + added = true; + } + } else { + const oldShiftIndex = layer.shifts.findIndex((shift) => shift.shiftId === updatingLayer.shifts[0].shiftId); + if (oldShiftIndex > -1) { + const newLayer = { ...layer }; + newLayer.shifts = [...layer.shifts]; + newLayer.shifts[oldShiftIndex] = updatingLayer.shifts[0]; + + memo[index] = newLayer; + + added = true; + } + } + + return layers; + }, layers); + + if (!added) { + layers.push(updatingLayer); + } + + return layers; +}; + +const L1_COLORS = ['#3D71D9', '#6D609C', '#4D3B72', '#8214A0']; + +const L2_COLORS = ['#3CB979', '#188343', '#84362A', '#521913']; + +const L3_COLORS = ['#377277', '#638282', '#364E4E', '#423220']; + +const OVERRIDE_COLORS = ['#C69B06', '#C2C837']; + +const COLORS = [L1_COLORS, L2_COLORS, L3_COLORS, OVERRIDE_COLORS]; + +export const getColor = (layerIndex: number, rotationIndex: number) => { + const normalizedLayerIndex = layerIndex % COLORS.length; + const normalizedRotationIndex = rotationIndex % COLORS[normalizedLayerIndex]?.length; + + return COLORS[normalizedLayerIndex]?.[normalizedRotationIndex]; +}; + +export const getOverrideColor = (rotationIndex: number) => { + const normalizedRotationIndex = rotationIndex % OVERRIDE_COLORS.length; + return OVERRIDE_COLORS[normalizedRotationIndex]; +}; diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index e73359ab..b625f5cd 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -11,7 +11,7 @@ import { makeRequest } from 'network'; import { RootStore } from 'state'; import { SelectOption } from 'state/types'; -import { fillGaps, splitToShiftsAndFillGaps } from './schedule.helpers'; +import { enrichLayers, fillGaps, getFromString, splitToLayers, splitToShiftsAndFillGaps } from './schedule.helpers'; import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event, Layer } from './schedule.types'; const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss'; @@ -73,7 +73,7 @@ export class ScheduleStore extends BaseStore { } = {}; @observable - rotationPreview?: Layer; + rotationPreview?: Layer[]; @observable finalPreview?: Array<{ shiftId: Shift['id']; events: Event[] }>; @@ -202,19 +202,25 @@ export class ScheduleStore extends BaseStore { ) { const type = isOverride ? 3 : 2; - const typeString = isOverride ? 'override' : 'rotation'; - const response = await makeRequest(`/oncall_shifts/preview/`, { params: { date: fromString }, data: { type, schedule: scheduleId, shift_pk: shiftId === 'new' ? undefined : shiftId, ...params }, method: 'POST', }).catch(this.onApiError); - this.rotationPreview = { - priority: params.priority_level, - shifts: [{ shiftId: shiftId, events: fillGaps(response.rotation.filter((event) => !event.is_gap)) }], - }; - this.finalPreview = splitToShiftsAndFillGaps(response.final).filter((shift) => shift.shiftId !== shiftId); + if (isOverride) { + } else { + const layers = enrichLayers( + [...(this.events[scheduleId]?.['rotation']?.[fromString] as Layer[])], + response.rotation, + shiftId, + params.priority_level + ); + + this.rotationPreview = layers; + } + + this.finalPreview = splitToShiftsAndFillGaps(response.final); /*.filter((shift) => shift.shiftId !== shiftId);*/ } async updateRotation(shiftId: Shift['id'], params: Partial) { @@ -345,31 +351,7 @@ export class ScheduleStore extends BaseStore { } });*/ - const layers: Layer[] | undefined = - type === 'rotation' - ? shifts - .reduce((memo, shift) => { - const storeShift = this.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 layers = type === 'rotation' ? splitToLayers(shifts) : undefined; this.events = { ...this.events, diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index 6bb2af11..572a8097 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -71,16 +71,16 @@ export type RotationType = 'final' | 'rotation' | 'override'; export interface Event { all_day: boolean; - calendar_type: 0; + calendar_type: ScheduleType; end: string; is_empty: boolean; is_gap: boolean; - missing_users: []; + missing_users: Array<{ display_name: User['username']; pk: User['pk'] }>; priority_level: number; - shift: { pk: string }; + shift: { pk: Shift['id'] | null }; source: string; start: string; - users: [{ display_name: User['username']; pk: User['pk'] }]; + users: Array<{ display_name: User['username']; pk: User['pk'] }>; } export interface Events {