Skip to content

Commit

Permalink
merge final schedule in as less lines as possible (#2649)
Browse files Browse the repository at this point in the history
# What this PR does

Merge final schedule in less rows

## Which issue(s) this PR fixes

[Final schedule shifts should lay in one
line](#1665)

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [ ] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
  • Loading branch information
Maxim Mordasov authored Aug 4, 2023
1 parent ceeb3b8 commit 8412282
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Shift Swap Requests Web UI ([#2593](https://github.com/grafana/oncall/issues/2593))
- Final schedule shifts should lay in one line [1665](https://github.com/grafana/oncall/issues/1665)

### Changed

Expand Down
10 changes: 6 additions & 4 deletions grafana-plugin/src/containers/Rotation/Rotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import hash from 'object-hash';

import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.types';
import ScheduleSlot from 'containers/ScheduleSlot/ScheduleSlot';
import { Schedule, Event, RotationFormLiveParams, ShiftSwap } from 'models/schedule/schedule.types';
import { Schedule, Event, RotationFormLiveParams, Shift, ShiftSwap } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';

import RotationTutorial from './RotationTutorial';
Expand All @@ -33,6 +33,7 @@ interface RotationProps {
tutorialParams?: RotationFormLiveParams;
simplified?: boolean;
filters?: ScheduleFiltersType;
getColor?: (shiftId: Shift['id']) => string;
onSlotClick?: (event: Event) => void;
}

Expand All @@ -42,7 +43,7 @@ const Rotation: FC<RotationProps> = (props) => {
scheduleId,
startMoment,
currentTimezone,
color,
color: propsColor,
days = 7,
transparent = false,
tutorialParams,
Expand All @@ -52,6 +53,7 @@ const Rotation: FC<RotationProps> = (props) => {
onShiftSwapClick,
simplified,
filters,
getColor,
onSlotClick,
} = props;

Expand Down Expand Up @@ -113,7 +115,7 @@ const Rotation: FC<RotationProps> = (props) => {
}, [events]);

return (
<div className={cx('root')} onClick={handleRotationClick}>
<div className={cx('root')} onClick={onClick && handleRotationClick}>
<div className={cx('timeline')}>
{tutorialParams && <RotationTutorial startMoment={startMoment} {...tutorialParams} />}
{events ? (
Expand All @@ -130,7 +132,7 @@ const Rotation: FC<RotationProps> = (props) => {
event={event}
startMoment={startMoment}
currentTimezone={currentTimezone}
color={color}
color={propsColor || getColor(event.shift?.pk)}
handleAddOverride={getAddOverrideClickHandler(event)}
handleAddShiftSwap={getAddShiftSwapClickHandler(event)}
onShiftSwapClick={onShiftSwapClick}
Expand Down
28 changes: 11 additions & 17 deletions grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.
import Text from 'components/Text/Text';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
import { getLayersFromStore, getOverridesFromStore, getShiftsFromStore } from 'models/schedule/schedule.helpers';
import {
flattenFinalShifs,
getLayersFromStore,
getOverridesFromStore,
getShiftsFromStore,
} from 'models/schedule/schedule.helpers';
import { Schedule, Shift, ShiftSwap, Event } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { WithStoreProps } from 'state/types';
Expand Down Expand Up @@ -55,14 +60,16 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState

const currentTimeX = diff / base;

const shifts = getShiftsFromStore(store, scheduleId, startMoment);
const shifts = flattenFinalShifs(getShiftsFromStore(store, scheduleId, startMoment));

const layers = getLayersFromStore(store, scheduleId, startMoment);

const overrides = getOverridesFromStore(store, scheduleId, startMoment);

const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;

const getColor = (shiftId: Shift['id']) => findColor(shiftId, layers, overrides);

return (
<>
<div className={cx('root')}>
Expand All @@ -82,7 +89,7 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
<TimelineMarks startMoment={startMoment} timezone={currentTimezone} />
<TransitionGroup className={cx('rotations')}>
{shifts && shifts.length ? (
shifts.map(({ shiftId, events }, index) => {
shifts.map(({ events }, index) => {
return (
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
<Rotation
Expand All @@ -91,13 +98,12 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
events={events}
startMoment={startMoment}
currentTimezone={currentTimezone}
color={findColor(shiftId, layers, overrides)}
onClick={this.getRotationClickHandler(shiftId)}
handleAddOverride={this.handleShowOverrideForm}
handleAddShiftSwap={onShowShiftSwapForm}
onShiftSwapClick={onShowShiftSwapForm}
simplified={simplified}
filters={filters}
getColor={getColor}
onSlotClick={onSlotClick}
/>
</CSSTransition>
Expand All @@ -120,18 +126,6 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
);
}

getRotationClickHandler = (shiftId: Shift['id']) => {
const { onClick, disabled } = this.props;

return () => {
if (disabled) {
return;
}

onClick(shiftId);
};
};

onSearchTermChangeCallback = () => {};

handleShowOverrideForm = (shiftStart: dayjs.Dayjs, shiftEnd: dayjs.Dayjs) => {
Expand Down
146 changes: 132 additions & 14 deletions grafana-plugin/src/models/schedule/schedule.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ export const getFromString = (moment: dayjs.Dayjs) => {
return moment.format('YYYY-MM-DD');
};

const createGap = (start, end) => {
return {
start,
end,
is_gap: true,
users: [],
all_day: false,
shift: null,
missing_users: [],
is_empty: true,
calendar_type: ScheduleType.API,
priority_level: null,
source: 'web',
is_override: false,
};
};

export const fillGaps = (events: Event[]) => {
const newEvents = [];

Expand All @@ -18,19 +35,7 @@ export const fillGaps = (events: Event[]) => {

if (nextEvent) {
if (nextEvent.start !== event.end) {
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',
});
newEvents.push(createGap(event.end, nextEvent.start));
}
}
}
Expand Down Expand Up @@ -69,6 +74,119 @@ export const getShiftsFromStore = (
: (store.scheduleStore.events[scheduleId]?.['final']?.[getFromString(startMoment)] as any);
};

export const flattenFinalShifs = (shifts: ShiftEvents[]) => {
if (!shifts) {
return undefined;
}

function splitToPairs(shifts: ShiftEvents[]) {
const pairs = [];
for (let i = 0; i < shifts.length - 1; i++) {
for (let j = i + 1; j < shifts.length; j++) {
pairs.push([
{ ...shifts[i], events: [...shifts[i].events] },
{ ...shifts[j], events: [...shifts[j].events] },
]);
}
}

return pairs;
}

let pairs = splitToPairs(shifts);

while (pairs.length > 0) {
const currentPair = pairs.shift();

const merged = mergePair(currentPair);

if (merged !== currentPair) {
// means pair was fully merged

shifts = shifts.filter((shift) => !currentPair.some((pairShift) => pairShift.shiftId === shift.shiftId));
shifts.unshift(merged[0]);
pairs = splitToPairs(shifts);
}
}

function mergePair(pair: ShiftEvents[]): ShiftEvents[] {
const recipient = { ...pair[0], events: [...pair[0].events] };
const donor = pair[1];

const donorEvents = donor.events.filter((event) => !event.is_gap);

for (let i = 0; i < donorEvents.length; i++) {
const donorEvent = donorEvents[i];

const eventStartMoment = dayjs(donorEvent.start);
const eventEndMoment = dayjs(donorEvent.end);

const suitablerRecepientGapIndex = recipient.events.findIndex((event) => {
if (!event.is_gap) {
return false;
}

const gap = event;

const gapStartMoment = dayjs(gap.start);
const gapEndMoment = dayjs(gap.end);

return gapStartMoment.isSameOrBefore(eventStartMoment) && gapEndMoment.isSameOrAfter(eventEndMoment);
});

if (suitablerRecepientGapIndex > -1) {
const suitablerRecepientGap = recipient.events[suitablerRecepientGapIndex];

const itemsToAdd = [];
const leftGap = createGap(suitablerRecepientGap.start, donorEvent.start);
if (leftGap.start !== leftGap.end) {
itemsToAdd.push(leftGap);
}
itemsToAdd.push(donorEvent);

const rightGap = createGap(donorEvent.end, suitablerRecepientGap.end);
if (rightGap.start !== rightGap.end) {
itemsToAdd.push(rightGap);
}

recipient.events = [
...recipient.events.slice(0, suitablerRecepientGapIndex),
...itemsToAdd,
...recipient.events.slice(suitablerRecepientGapIndex + 1),
];
} else {
const firstRecepientEvent = recipient.events[0];
const firstRecepientEventStartMoment = dayjs(firstRecepientEvent.start);

const lastRecepientEvent = recipient.events[recipient.events.length - 1];
const lastRecepientEventEndMoment = dayjs(lastRecepientEvent.end);

if (eventEndMoment.isSameOrBefore(firstRecepientEventStartMoment)) {
const itemsToAdd = [donorEvent];
if (donorEvent.end !== firstRecepientEvent.start) {
itemsToAdd.push(createGap(donorEvent.end, firstRecepientEvent.start));
}
recipient.events = [...itemsToAdd, ...recipient.events];
} else if (eventStartMoment.isSameOrAfter(lastRecepientEventEndMoment)) {
const itemsToAdd = [donorEvent];
if (lastRecepientEvent.end !== donorEvent.start) {
itemsToAdd.unshift(createGap(lastRecepientEvent.end, donorEvent.start));
}
recipient.events = [...recipient.events, ...itemsToAdd];
} else {
// the pair can't be fully merged

return pair;
}
}
}

return [recipient];
}

return shifts;
};

export const getLayersFromStore = (store: RootStore, scheduleId: Schedule['id'], startMoment: dayjs.Dayjs): Layer[] => {
return store.scheduleStore.rotationPreview
? store.scheduleStore.rotationPreview[getFromString(startMoment)]
Expand All @@ -79,7 +197,7 @@ export const getOverridesFromStore = (
store: RootStore,
scheduleId: Schedule['id'],
startMoment: dayjs.Dayjs
): Layer[] | ShiftEvents[] => {
): ShiftEvents[] => {
return store.scheduleStore.overridePreview
? store.scheduleStore.overridePreview[getFromString(startMoment)]
: (store.scheduleStore.events[scheduleId]?.['override']?.[getFromString(startMoment)] as Layer[]);
Expand Down
1 change: 1 addition & 0 deletions grafana-plugin/src/models/schedule/schedule.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export interface Layer {
export interface ShiftEvents {
shiftId: string;
events: Event[];
priority: number;
isPreview?: boolean;
}

Expand Down
13 changes: 9 additions & 4 deletions grafana-plugin/src/pages/schedule/Schedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,12 +283,17 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
scheduleId={scheduleId}
currentTimezone={currentTimezone}
startMoment={startMoment}
onClick={this.handleShowForm}
disabled={disabledRotationForm}
onShowOverrideForm={this.handleShowOverridesForm}
filters={filters}
onShowShiftSwapForm={this.handleShowShiftSwapForm}
onSlotClick={shiftSwapIdToShowForm ? this.onSlotClick : undefined}
onSlotClick={
shiftSwapIdToShowForm
? this.adjustShiftSwapForm
: (event: Event) => {
this.handleShowForm(event.shift.pk);
}
}
/>
<Rotations
scheduleId={scheduleId}
Expand All @@ -303,7 +308,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
disabled={disabledRotationForm}
filters={filters}
onShowShiftSwapForm={this.handleShowShiftSwapForm}
onSlotClick={shiftSwapIdToShowForm ? this.onSlotClick : undefined}
onSlotClick={shiftSwapIdToShowForm ? this.adjustShiftSwapForm : undefined}
/>
<ScheduleOverrides
scheduleId={scheduleId}
Expand Down Expand Up @@ -595,7 +600,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
this.setState({ shiftSwapIdToShowForm: undefined, shiftSwapParamsToShowForm: undefined });
};

onSlotClick = (event: Event) => {
adjustShiftSwapForm = (event: Event) => {
this.setState({
shiftSwapParamsToShowForm: {
...this.state.shiftSwapParamsToShowForm,
Expand Down

0 comments on commit 8412282

Please sign in to comment.