Skip to content

Commit

Permalink
Schedule two-weeks and monthly view (#4318)
Browse files Browse the repository at this point in the history
# What this PR does

Schedule two-weeks and monthly view, UI polishing

## Which issue(s) this PR closes

https://github.com/grafana/oncall-private/issues/2667


## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.

---------

Co-authored-by: Dominik Broj <[email protected]>
  • Loading branch information
Maxim Mordasov and brojd authored May 30, 2024
1 parent 8559112 commit b294653
Show file tree
Hide file tree
Showing 32 changed files with 921 additions and 598 deletions.
8 changes: 0 additions & 8 deletions .config/webpack/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,6 @@ const config = async (env): Promise<Configuration> => {
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset/resource',
Expand Down
31 changes: 31 additions & 0 deletions e2e-tests/schedules/scheduleView.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { scheduleViewToDaysInOneRow } from 'models/schedule/schedule.helpers';
import { ScheduleView } from 'models/schedule/schedule.types';
import { HTML_ID } from 'utils/DOM';

import { expect, test } from '../fixtures';
import { generateRandomValue } from '../utils/forms';
import { createOnCallScheduleWithRotation } from '../utils/schedule';

test.skip('schedule view (week/2 weeks/month) toggler works', async ({ adminRolePage }) => {
const { page, userName } = adminRolePage;

const onCallScheduleName = generateRandomValue();
await createOnCallScheduleWithRotation(page, onCallScheduleName, userName);

// ScheduleView.OneWeek is selected by default
expect(await page.getByLabel(ScheduleView.OneWeek, { exact: true }).isChecked()).toBe(true);

expect(await page.locator(`#${HTML_ID.SCHEDULE_FINAL} .TEST_weekday`).count()).toStrictEqual(
scheduleViewToDaysInOneRow[ScheduleView.OneWeek]
);

await page.getByLabel(ScheduleView.TwoWeeks, { exact: true }).click();
expect(await page.getByLabel(ScheduleView.TwoWeeks, { exact: true }).isChecked()).toBe(true);
expect(await page.locator(`#${HTML_ID.SCHEDULE_FINAL} .TEST_weekday`).count()).toStrictEqual(
scheduleViewToDaysInOneRow[ScheduleView.TwoWeeks]
);

await page.getByLabel(ScheduleView.OneMonth, { exact: true }).click();
expect(await page.getByLabel(ScheduleView.OneMonth, { exact: true }).isChecked()).toBe(true);
expect(await page.locator(`#${HTML_ID.SCHEDULE_FINAL} .TEST_weekday`).count()).toBeGreaterThanOrEqual(28);
});
2 changes: 2 additions & 0 deletions src/assets/style/vars.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
--hover-selected: #f4f5f5;
--background-canvas: #f4f5f5;
--background-secondary: #f4f5f5;
--background-disabled: rgba(204, 204, 220, 0.11);
--border-medium-color: rgba(36, 41, 46, 0.3);
--border-medium: 1px solid rgba(36, 41, 46, 0.3);
--border-strong: 1px solid rgba(36, 41, 46, 0.4);
Expand Down Expand Up @@ -61,6 +62,7 @@
--oncall-icon-stroke-color: #181b1f;
--background-canvas: #111217;
--background-secondary: #22252b;
--background-disabled: rgba(204, 204, 220, 0.04);
--border-medium-color: rgba(204, 204, 220, 0.15);
--border-medium: 1px solid rgba(204, 204, 220, 0.15);
--border-strong: 1px solid rgba(204, 204, 220, 0.25);
Expand Down
25 changes: 0 additions & 25 deletions src/containers/Rotation/Rotation.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@
position: relative;
}

.root:last-child .timeline {
padding-bottom: 0;
}

.slots {
width: 100%;
display: flex;
Expand All @@ -39,10 +35,6 @@
opacity: 0.5;
}

.slots__animate {
transition: transform 500ms ease;
}

.current-time {
position: absolute;
left: 450px;
Expand All @@ -64,11 +56,6 @@
margin: 0 2px;
}

.slots--tutorial {
position: absolute;
background: rgba(61, 113, 217, 0.15);
}

.pointer {
position: absolute;
top: -9px;
Expand All @@ -82,15 +69,3 @@
transform: scale(1);
opacity: 1;
}

.tutorial-slot {
width: 175px;
height: 28px;
border-radius: 2px;
margin: 0 1px;
padding: 4px;
}

.tutorial-slot--active {
box-shadow: var(--shadows-z3);
}
34 changes: 19 additions & 15 deletions src/containers/Rotation/Rotation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useMemo, useState } from 'react';
import React, { FC, useMemo } from 'react';

import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
Expand All @@ -9,11 +9,10 @@ import hash from 'object-hash';
import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.types';
import { Text } from 'components/Text/Text';
import { ScheduleSlot } from 'containers/ScheduleSlot/ScheduleSlot';
import { Event, RotationFormLiveParams, ShiftSwap } from 'models/schedule/schedule.types';
import { scheduleViewToDaysInOneRow } from 'models/schedule/schedule.helpers';
import { Event, ScheduleView, ShiftSwap } from 'models/schedule/schedule.types';
import { useStore } from 'state/useStore';

import { RotationTutorial } from './RotationTutorial';

import styles from './Rotation.module.css';

const cx = cn.bind(styles);
Expand All @@ -28,27 +27,26 @@ interface RotationProps {
handleAddShiftSwap?: (id: 'new', params: Partial<ShiftSwap>) => void;
handleOpenSchedule?: (event: Event) => void;
onShiftSwapClick?: (swapId: ShiftSwap['id']) => void;
days?: number;
transparent?: boolean;
tutorialParams?: RotationFormLiveParams;
simplified?: boolean;
filters?: ScheduleFiltersType;
getColor?: (event: Event) => string;
onSlotClick?: (event: Event) => void;
emptyText?: string;
showScheduleNameAsSlotTitle?: boolean;
startDate?: dayjs.Dayjs;
scheduleView?: ScheduleView;
}

export const Rotation: FC<RotationProps> = observer((props) => {
const {
timezoneStore: { calendarStartDate, getDateInSelectedTimezone },
scheduleStore: { scheduleView: storeScheduleView },
} = useStore();
const {
events,
color: propsColor,
days = 7,
transparent = false,
tutorialParams,
onClick,
handleAddOverride,
handleAddShiftSwap,
Expand All @@ -60,18 +58,24 @@ export const Rotation: FC<RotationProps> = observer((props) => {
onSlotClick,
emptyText,
showScheduleNameAsSlotTitle,
startDate: propsStartDate,
scheduleView: propsScheduleView,
} = props;

const [animate, _setAnimate] = useState<boolean>(true);
const scheduleView = propsScheduleView || storeScheduleView;

const startDate = propsStartDate || calendarStartDate;

const days = scheduleViewToDaysInOneRow[scheduleView];

const handleRotationClick = (event: React.MouseEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left; //x position within the element.
const width = event.currentTarget.offsetWidth;

const dayOffset = Math.floor((x / width) * 7);
const dayOffset = Math.floor((x / width) * scheduleViewToDaysInOneRow[scheduleView]);

const shiftStart = calendarStartDate.add(dayOffset, 'day');
const shiftStart = startDate.add(dayOffset, 'day');
const shiftEnd = shiftStart.add(1, 'day');

onClick(shiftStart, shiftEnd);
Expand Down Expand Up @@ -134,27 +138,27 @@ export const Rotation: FC<RotationProps> = observer((props) => {

const firstShift = events[0];
const firstShiftOffset = getDateInSelectedTimezone(firstShift.start).diff(
getDateInSelectedTimezone(calendarStartDate),
getDateInSelectedTimezone(startDate),
'seconds'
);
const base = 60 * 60 * 24 * days;

return firstShiftOffset / base;
}, [events, calendarStartDate]);
}, [events, startDate]);

return (
<div className={cx('root')} onClick={onClick && handleRotationClick}>
<div className={cx('timeline')}>
{tutorialParams && <RotationTutorial {...tutorialParams} />}
{events ? (
events.length ? (
<div
className={cx('slots', { slots__animate: animate, slots__transparent: transparent })}
className={cx('slots', { slots__transparent: transparent })}
style={{ transform: `translate(${x * 100}%, 0)` }}
>
{events.map((event) => {
return (
<ScheduleSlot
scheduleView={scheduleView}
key={hash(event)}
event={event}
color={propsColor || getColor(event)}
Expand Down
117 changes: 0 additions & 117 deletions src/containers/Rotation/RotationTutorial.tsx

This file was deleted.

7 changes: 5 additions & 2 deletions src/containers/RotationForm/RotationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const RotationForm = observer((props: RotationFormProps) => {

const [rotationName, setRotationName] = useState<string>(`[L${layerPriority}] Rotation`);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [offsetTop, setOffsetTop] = useState<number>(0);
const [offsetTop, setOffsetTop] = useState<number>(GRAFANA_HEADER_HEIGHT + 10);
const [draggablePosition, setDraggablePosition] = useState<{ x: number; y: number }>(undefined);

const [shiftStart, setShiftStart] = useState<dayjs.Dayjs>(propsShiftStart);
Expand Down Expand Up @@ -198,7 +198,10 @@ export const RotationForm = observer((props: RotationFormProps) => {
} catch (err) {
onError(err);
} finally {
setIsOpen(true);
// wait until a scroll to the "Rotations" happened
setTimeout(() => {
setIsOpen(true);
}, 100);
}
};

Expand Down
9 changes: 6 additions & 3 deletions src/containers/RotationForm/ScheduleOverrideForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Schedule, Shift } from 'models/schedule/schedule.types';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers';
import { useStore } from 'state/useStore';
import { getCoords, waitForElement } from 'utils/DOM';
import { HTML_ID, getCoords, waitForElement } from 'utils/DOM';
import { GRAFANA_HEADER_HEIGHT } from 'utils/consts';
import { useDebouncedCallback } from 'utils/hooks';

Expand Down Expand Up @@ -79,7 +79,7 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
useEffect(() => {
(async () => {
if (isOpen) {
const elm = await waitForElement('#overrides-list');
const elm = await waitForElement(`#${HTML_ID.SCHEDULE_OVERRIDES_AND_SWAPS}`);
const modal = document.querySelector(`.${cx('draggable')}`) as HTMLDivElement;
const coords = getCoords(elm);
const offsetTop = Math.min(
Expand Down Expand Up @@ -171,7 +171,10 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
} catch (err) {
onError(err);
} finally {
setIsOpen(true);
// wait until a scroll to the "Overrides and swaps" happened
setTimeout(() => {
setIsOpen(true);
}, 100);
}
};

Expand Down
Loading

0 comments on commit b294653

Please sign in to comment.