add working hours component
This commit is contained in:
parent
f6ababca7d
commit
b2f693b835
12 changed files with 272 additions and 53 deletions
|
|
@ -3,6 +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';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
|
|
@ -21,6 +24,9 @@ import { useNavModel } from 'utils/hooks';
|
|||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(weekday);
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
|
||||
// dayjs().weekday(0);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,12 +39,12 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
|
||||
const layers = [
|
||||
{ id: 0, title: 'Layer 1' },
|
||||
{ id: 1, title: 'Layer 2' },
|
||||
/* { id: 1, title: 'Layer 2' },
|
||||
{ id: 2, title: 'Layer 3' },
|
||||
{ id: 3, title: 'Layer 4' },
|
||||
{ id: 3, title: 'Layer 4' },*/
|
||||
];
|
||||
|
||||
const rotations = [{}, {}];
|
||||
const rotations = [{} /*, {}*/];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@
|
|||
gap: 4px;
|
||||
}
|
||||
|
||||
.working-hours{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
|
|
@ -22,6 +27,7 @@
|
|||
background: rgba(209, 14, 92, 0.2);
|
||||
border: 1px dashed #FF5286;
|
||||
color: rgba(209, 14, 92, .5);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.root__inactive {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { observer } from 'mobx-react';
|
|||
|
||||
import Line from 'components/ScheduleUserDetails/img/line.svg';
|
||||
import Text from 'components/Text/Text';
|
||||
import WorkingHours from 'components/WorkingHours/WorkingHours';
|
||||
import { Shift } from 'models/schedule/schedule.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -57,7 +58,15 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
backgroundColor: color,
|
||||
}}
|
||||
>
|
||||
<div style={{ left: `${left}%`, right: `${right}%` }} className={cx('striped')} />
|
||||
{storeUser && (
|
||||
<WorkingHours
|
||||
className={cx('working-hours')}
|
||||
timezone={storeUser.timezone}
|
||||
workingHours={storeUser.working_hours}
|
||||
startMoment={shift.start}
|
||||
duration={shift.duration}
|
||||
/>
|
||||
)}
|
||||
{label && (
|
||||
<div className={cx('label')} style={{ color }}>
|
||||
{label}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
export const default_working_hours = {
|
||||
friday: [{ end: '17:00:00', start: '09:00:00' }],
|
||||
monday: [{ end: '17:00:00', start: '09:00:00' }],
|
||||
sunday: [],
|
||||
tuesday: [{ end: '17:00:00', start: '09:00:00' }],
|
||||
saturday: [],
|
||||
thursday: [{ end: '17:00:00', start: '09:00:00' }],
|
||||
wednesday: [{ end: '17:00:00', start: '09:00:00' }],
|
||||
};
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
export const getWorkingMoments = (
|
||||
startMoment,
|
||||
endMoment,
|
||||
workingHours,
|
||||
timezone,
|
||||
) => {
|
||||
const weekdays = dayjs.weekdays();
|
||||
|
||||
const dayOfWeekToStartIteration = startMoment.format('dddd');
|
||||
const weekDaysToIterateChunk = [
|
||||
dayOfWeekToStartIteration,
|
||||
...weekdays.slice(weekdays.indexOf(dayOfWeekToStartIteration) + 1),
|
||||
...weekdays.slice(0, weekdays.indexOf(dayOfWeekToStartIteration)),
|
||||
];
|
||||
|
||||
const weeks = endMoment.diff(startMoment, 'weeks');
|
||||
|
||||
const weekDaysToIterate = [...weekDaysToIterateChunk];
|
||||
for (let i = 0; i < weeks; i++) {
|
||||
weekDaysToIterate.push(...weekDaysToIterateChunk);
|
||||
}
|
||||
|
||||
const workingMoments = [];
|
||||
for (const [i, weekday] of weekDaysToIterate.entries()) {
|
||||
for (const range of workingHours[weekday.toLowerCase()]) {
|
||||
const rangeStartData = range.start;
|
||||
const rangeEndData = range.end;
|
||||
const [start_HH, start_mm, start_ss] = rangeStartData.split(':');
|
||||
const [end_HH, end_mm, end_ss] = rangeEndData.split(':');
|
||||
|
||||
const rangeStartMoment = dayjs(startMoment)
|
||||
.tz(timezone)
|
||||
.add(i, 'day')
|
||||
.set('hour', Number(start_HH))
|
||||
.set('minute', Number(start_mm))
|
||||
.set('second', Number(start_ss));
|
||||
|
||||
const rangeEndMoment = dayjs(startMoment)
|
||||
.tz(timezone)
|
||||
.add(i, 'day')
|
||||
.set('hour', Number(end_HH))
|
||||
.set('minute', Number(end_mm))
|
||||
.set('second', Number(end_ss));
|
||||
|
||||
if (rangeEndMoment.isSameOrBefore(startMoment)) {
|
||||
continue;
|
||||
} else if (rangeStartMoment.isSameOrAfter(endMoment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
rangeStartMoment.isSameOrBefore(startMoment) &&
|
||||
rangeEndMoment.isSameOrAfter(startMoment) &&
|
||||
rangeEndMoment.isSameOrBefore(endMoment)
|
||||
) {
|
||||
workingMoments.push({ start: startMoment, end: rangeEndMoment });
|
||||
} else if (
|
||||
rangeEndMoment.isSameOrAfter(endMoment) &&
|
||||
rangeStartMoment.isSameOrBefore(endMoment) &&
|
||||
rangeStartMoment.isSameOrAfter(startMoment)
|
||||
) {
|
||||
workingMoments.push({ start: rangeStartMoment, end: endMoment });
|
||||
} else {
|
||||
workingMoments.push({ start: rangeStartMoment, end: rangeEndMoment });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return workingMoments;
|
||||
};
|
||||
|
||||
export const getNonWorkingMoments = (startMoment, endMoment, workingHours) => {
|
||||
const nonWorkingMoments = [{ start: startMoment, end: endMoment }];
|
||||
|
||||
let lastNonWorkingRange = nonWorkingMoments[0];
|
||||
for (const [i, range] of workingHours.entries()) {
|
||||
lastNonWorkingRange.end = range.start;
|
||||
|
||||
lastNonWorkingRange = { start: range.end, end: undefined };
|
||||
nonWorkingMoments.push(lastNonWorkingRange);
|
||||
}
|
||||
|
||||
lastNonWorkingRange.end = endMoment;
|
||||
|
||||
return nonWorkingMoments;
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
89
grafana-plugin/src/components/WorkingHours/WorkingHours.tsx
Normal file
89
grafana-plugin/src/components/WorkingHours/WorkingHours.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
|
||||
import { default_working_hours } from './WorkingHours.config';
|
||||
import { getNonWorkingMoments, getWorkingMoments } from './WorkingHours.helpers';
|
||||
|
||||
import styles from './WorkingHours.module.css';
|
||||
|
||||
import { start } from 'repl';
|
||||
|
||||
interface WorkingHoursProps {
|
||||
timezone: Timezone;
|
||||
workingHours: any;
|
||||
startMoment: dayjs.Dayjs;
|
||||
duration: number; // in seconds
|
||||
width: number; // in pixels
|
||||
className: string;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const WorkingHours: FC<WorkingHoursProps> = (props) => {
|
||||
const {
|
||||
timezone,
|
||||
workingHours = default_working_hours,
|
||||
startMoment = dayjs().utc().startOf('week'),
|
||||
duration = 14 * 24 * 60 * 60,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
timezone = dayjs.tz.guess();
|
||||
|
||||
const endMoment = startMoment.add(duration, 'seconds');
|
||||
|
||||
const workingMoments = useMemo(
|
||||
() => getWorkingMoments(startMoment, endMoment, workingHours, timezone),
|
||||
[startMoment, endMoment, workingHours, timezone]
|
||||
);
|
||||
|
||||
const nonWorkingMoments = getNonWorkingMoments(startMoment, endMoment, workingMoments);
|
||||
|
||||
console.log(startMoment.tz(timezone).format('D MMM ddd HH:ss'));
|
||||
|
||||
/*console.log(
|
||||
workingMoments.map(
|
||||
(range) =>
|
||||
`${range.start.tz(timezone).format('D MMM ddd HH:ss')} - ${range.end.tz(timezone).format('D MMM ddd HH:ss')}`
|
||||
)
|
||||
);
|
||||
|
||||
console.log(
|
||||
nonWorkingMoments.map(
|
||||
(range) =>
|
||||
`${range.start.tz(timezone).format('D MMM ddd HH:ss')} - ${range.end.tz(timezone).format('D MMM ddd HH:ss')}`
|
||||
)
|
||||
);*/
|
||||
|
||||
return (
|
||||
<svg version="1.1" width="100%" height="28px" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<defs>
|
||||
<pattern id="stripes" patternUnits="userSpaceOnUse" width="10" height="10" patternTransform="rotate(45)">
|
||||
<line x1="0" y="0" x2="0" y2="10" stroke="rgba(17, 18, 23, 0.15)" strokeWidth="10" />
|
||||
</pattern>
|
||||
</defs>
|
||||
{nonWorkingMoments.map((moment, index) => {
|
||||
const start = moment.start.diff(startMoment, 'seconds');
|
||||
const diff = moment.end.diff(moment.start, 'seconds');
|
||||
return (
|
||||
<rect
|
||||
className={cx('stripes')}
|
||||
key={index}
|
||||
x={`${(start * 100) / duration}%`}
|
||||
y={0}
|
||||
width={`${(diff * 100) / duration}%`}
|
||||
height="100%"
|
||||
fill="url(#stripes)"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkingHours;
|
||||
|
|
@ -118,20 +118,24 @@ export class ScheduleStore extends BaseStore {
|
|||
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',
|
||||
'UQEAACAGQ5JHL',
|
||||
'UEHYTCX4AMX75',
|
||||
'U3U8343UTJ91U',
|
||||
'UTNF7TCGBPADM',
|
||||
'UWPPUTZHCC9U5',
|
||||
'UDUG977U8V8AX',
|
||||
'UNN22BHCXZ6TR',
|
||||
'UTKBFZH8HM1TF',
|
||||
'U1DJX6WMFTWY7',
|
||||
'UPZ7AJPKVJL9K',
|
||||
];
|
||||
|
||||
if (rnd > 0.33) {
|
||||
|
|
@ -151,8 +155,9 @@ export class ScheduleStore extends BaseStore {
|
|||
const shifts = [];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
shifts.push({
|
||||
start: dayjs(startMoment).add(3 * i, 'hour'),
|
||||
duration: (Math.floor(Math.random() * 6) + 10) * 60 * 60,
|
||||
start: dayjs(startMoment).add(12 * i, 'hour'),
|
||||
//duration: (Math.floor(Math.random() * 6) + 10) * 60 * 60,
|
||||
duration: 12 * 60 * 60,
|
||||
users: getUsers(),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export class UserStore extends BaseStore {
|
|||
...acc,
|
||||
[item.pk]: {
|
||||
...item,
|
||||
tz: getRandomTimezone(),
|
||||
timezone: getRandomTimezone(),
|
||||
working_hours: {
|
||||
monday: [{ start: '09:00:00', end: '18:00:00' }],
|
||||
tuesday: [{ start: '09:00:00', end: '18:00:00' }],
|
||||
|
|
@ -116,8 +116,6 @@ export class UserStore extends BaseStore {
|
|||
),
|
||||
};
|
||||
|
||||
console.log(this.items);
|
||||
|
||||
this.searchResult = {
|
||||
count,
|
||||
results: results.map((item: User) => item.pk),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.header{
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.desc{
|
||||
width: 736px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,43 +62,45 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
return (
|
||||
<div className={cx('root')}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<PluginLink query={{ page: 'schedules' }}>
|
||||
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xxl" />
|
||||
</PluginLink>
|
||||
<Text.Title level={3}>Schedule Team {query.id}</Text.Title>
|
||||
<ScheduleCounter
|
||||
type="link"
|
||||
count={5}
|
||||
tooltipTitle="Used in escalations"
|
||||
tooltipContent={
|
||||
<>
|
||||
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 1</PluginLink>
|
||||
<br />
|
||||
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 2</PluginLink>
|
||||
<br />
|
||||
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 3</PluginLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ScheduleCounter
|
||||
type="warning"
|
||||
count={2}
|
||||
tooltipTitle="Warnings"
|
||||
tooltipContent="Schedule has unassigned time periods during next 7 days"
|
||||
/>
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<PluginLink query={{ page: 'schedules' }}>
|
||||
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xxl" />
|
||||
</PluginLink>
|
||||
<Text.Title level={3}>Schedule Team {query.id}</Text.Title>
|
||||
<ScheduleCounter
|
||||
type="link"
|
||||
count={5}
|
||||
tooltipTitle="Used in escalations"
|
||||
tooltipContent={
|
||||
<>
|
||||
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 1</PluginLink>
|
||||
<br />
|
||||
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 2</PluginLink>
|
||||
<br />
|
||||
<PluginLink query={{ page: 'integrations', id: 'CXBEG63MBJMDL' }}>Grafana 3</PluginLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ScheduleCounter
|
||||
type="warning"
|
||||
count={2}
|
||||
tooltipTitle="Warnings"
|
||||
tooltipContent="Schedule has unassigned time periods during next 7 days"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<UserTimezoneSelect value={tz} users={users} onChange={this.handleTimezoneChange} />
|
||||
<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" />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<UserTimezoneSelect value={tz} users={users} onChange={this.handleTimezoneChange} />
|
||||
<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" />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<Text className={cx('desc')} size="small" type="secondary">
|
||||
On-call Schedules. Use this to distribute notifications among team members you specified in the "Notify
|
||||
Users from on-call schedule" step in escalation chains.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue