# What this PR does Polish ‘When I am on-call’ feature ## Which issue(s) this PR fixes [Build ‘When I am on-call’ for web UI #2915](https://github.com/grafana/oncall/issues/2915) ## 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)
This commit is contained in:
parent
35620028cc
commit
2d656c50db
11 changed files with 208 additions and 76 deletions
|
|
@ -7,9 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Use shift data from event object
|
||||
|
||||
### Fixed
|
||||
|
||||
- Update ical schedule creation/update to trigger final schedule refresh ([#3156](https://github.com/grafana/oncall/pull/3156))
|
||||
- Polish "Build 'When I am on-call' for web UI" [#2915](https://github.com/grafana/oncall/issues/2915)
|
||||
- Fix iCal schedule incorrect view [#2001](https://github.com/grafana/oncall-private/issues/2001)
|
||||
- Fix rotation name rendering issue [#2324](https://github.com/grafana/oncall/issues/2324)
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ 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, Shift, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { Event, RotationFormLiveParams, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
|
||||
import RotationTutorial from './RotationTutorial';
|
||||
|
|
@ -34,7 +34,7 @@ interface RotationProps {
|
|||
tutorialParams?: RotationFormLiveParams;
|
||||
simplified?: boolean;
|
||||
filters?: ScheduleFiltersType;
|
||||
getColor?: (shiftId: Shift['id']) => string;
|
||||
getColor?: (event: Event) => string;
|
||||
onSlotClick?: (event: Event) => void;
|
||||
emptyText?: string;
|
||||
showScheduleNameAsSlotTitle?: boolean;
|
||||
|
|
@ -156,7 +156,7 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
event={event}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
color={propsColor || getColor(event.shift?.pk)}
|
||||
color={propsColor || getColor(event)}
|
||||
handleAddOverride={getAddOverrideClickHandler(event)}
|
||||
handleAddShiftSwap={getAddShiftSwapClickHandler(event)}
|
||||
handleOpenSchedule={getOpenScheduleClickHandler(event)}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
getOverridesFromStore,
|
||||
getShiftsFromStore,
|
||||
} from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift, ShiftSwap, Event } from 'models/schedule/schedule.types';
|
||||
import { Schedule, ShiftSwap, Event } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -59,7 +59,7 @@ class ScheduleFinal extends Component<ScheduleFinalProps> {
|
|||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const getColor = (shiftId: Shift['id']) => findColor(shiftId, layers, overrides);
|
||||
const getColor = (event: Event) => findColor(event.shift?.pk, layers, overrides);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ interface ScheduleOverridesProps extends WithStoreProps {
|
|||
onUpdate: () => void;
|
||||
onDelete: () => void;
|
||||
disabled: boolean;
|
||||
disableShiftSwaps: boolean;
|
||||
filters: ScheduleFiltersType;
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +73,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
store,
|
||||
shiftIdToShowRotationForm,
|
||||
disabled,
|
||||
disableShiftSwaps,
|
||||
shiftStartToShowOverrideForm: propsShiftStartToShowOverrideForm,
|
||||
shiftEndToShowOverrideForm: propsShiftEndToShowOverrideForm,
|
||||
onShowShiftSwapForm,
|
||||
|
|
@ -112,7 +114,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
<HorizontalGroup>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={disabled}
|
||||
disabled={disableShiftSwaps}
|
||||
onClick={() => {
|
||||
const closestEvent = findClosestUserEvent(dayjs(), currentUserPk, layers);
|
||||
const swapStart = closestEvent
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Badge, HorizontalGroup } from '@grafana/ui';
|
||||
import { Badge, Button, HorizontalGroup, Icon } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -12,9 +12,10 @@ import Text from 'components/Text/Text';
|
|||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import Rotation from 'containers/Rotation/Rotation';
|
||||
import { getColorForSchedule, getPersonalShiftsFromStore } from 'models/schedule/schedule.helpers';
|
||||
import { Shift, Event } from 'models/schedule/schedule.types';
|
||||
import { Event } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
|
|
@ -32,24 +33,69 @@ interface SchedulePersonalProps extends WithStoreProps, RouteComponentProps {
|
|||
onSlotClick?: (event: Event) => void;
|
||||
}
|
||||
|
||||
@observer
|
||||
class SchedulePersonal extends Component<SchedulePersonalProps> {
|
||||
componentDidMount() {
|
||||
const { store, startMoment } = this.props;
|
||||
interface SchedulePersonalState {
|
||||
startMoment?: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment);
|
||||
@observer
|
||||
class SchedulePersonal extends Component<SchedulePersonalProps, SchedulePersonalState> {
|
||||
state: SchedulePersonalState = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
startMoment: props.startMoment,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<SchedulePersonalProps>): void {
|
||||
const { store, startMoment } = this.props;
|
||||
componentDidMount() {
|
||||
const { store } = this.props;
|
||||
const { startMoment } = this.state;
|
||||
|
||||
if (prevProps.startMoment !== this.props.startMoment) {
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment, 9, true);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<SchedulePersonalProps>, prevState: Readonly<SchedulePersonalState>): void {
|
||||
const { store } = this.props;
|
||||
const { startMoment } = this.state;
|
||||
|
||||
if (prevProps.currentTimezone !== this.props.currentTimezone) {
|
||||
const oldTimezone = prevProps.currentTimezone;
|
||||
|
||||
this.setState((oldState) => {
|
||||
const wDiff = oldState.startMoment.diff(getStartOfWeek(oldTimezone), 'weeks');
|
||||
|
||||
return { ...oldState, startMoment: getStartOfWeek(this.props.currentTimezone).add(wDiff, 'weeks') };
|
||||
});
|
||||
}
|
||||
|
||||
if (prevState.startMoment !== startMoment) {
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment);
|
||||
}
|
||||
}
|
||||
|
||||
handleTodayClick = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.setState({ startMoment: getStartOfWeek(store.currentTimezone) });
|
||||
};
|
||||
|
||||
handleLeftClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
|
||||
this.setState({ startMoment: startMoment.add(-7, 'day') });
|
||||
};
|
||||
|
||||
handleRightClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
|
||||
this.setState({ startMoment: startMoment.add(7, 'day') });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { userPk, startMoment, currentTimezone, store, onSlotClick } = this.props;
|
||||
const { userPk, currentTimezone, store, onSlotClick } = this.props;
|
||||
const { startMoment } = this.state;
|
||||
|
||||
const base = 7 * 24 * 60; // in minutes
|
||||
const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes');
|
||||
|
|
@ -60,18 +106,7 @@ class SchedulePersonal extends Component<SchedulePersonalProps> {
|
|||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const getColor = (shiftId: Shift['id']) => {
|
||||
const shift = store.scheduleStore.shifts[shiftId];
|
||||
|
||||
if (!shift) {
|
||||
if (shiftId) {
|
||||
store.scheduleStore.updateOncallShift(shiftId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return getColorForSchedule(shift.schedule);
|
||||
};
|
||||
const getColor = (event: Event) => getColorForSchedule(event.schedule?.id);
|
||||
|
||||
const isOncall = store.scheduleStore.onCallNow[userPk];
|
||||
|
||||
|
|
@ -82,12 +117,37 @@ class SchedulePersonal extends Component<SchedulePersonalProps> {
|
|||
<div className={cx('root')}>
|
||||
<div className={cx('header')}>
|
||||
<div className={cx('title')}>
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">
|
||||
On-call schedule <Avatar src={storeUser.avatar} size="small" /> {store.userStore.currentUser.name}
|
||||
</Text>
|
||||
{/* @ts-ignore */}
|
||||
{isOncall ? <Badge text="On-call now" color="green" /> : <Badge text="Not on-call now" color="gray" />}
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">
|
||||
On-call schedule <Avatar src={storeUser.avatar} size="small" /> {storeUser.username}
|
||||
</Text>
|
||||
|
||||
{isOncall ? (
|
||||
<Badge text="On-call now" color="green" />
|
||||
) : (
|
||||
/* @ts-ignore */
|
||||
<Badge text="Not on-call now" color="gray" />
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">
|
||||
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
|
||||
</Text>
|
||||
<Button variant="secondary" size="sm" onClick={this.handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Button variant="secondary" size="sm" onClick={this.handleLeftClick}>
|
||||
<Icon name="angle-left" />
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={this.handleRightClick}>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
|
||||
const base = 60 * 60 * 24 * 7;
|
||||
|
||||
const width = duration / base;
|
||||
const width = Math.max(duration / base, 0);
|
||||
|
||||
const currentMoment = useMemo(() => dayjs(), []);
|
||||
|
||||
|
|
@ -172,6 +172,7 @@ const ShiftSwapEvent = (props: ShiftSwapEventProps) => {
|
|||
content={
|
||||
<ScheduleSlotDetails
|
||||
isShiftSwap
|
||||
title="Shift swap"
|
||||
beneficiaryName={beneficiary?.display_name}
|
||||
user={benefactorStoreUser || beneficiaryStoreUser}
|
||||
benefactorName={benefactor?.display_name}
|
||||
|
|
@ -237,13 +238,17 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
{users.map(({ display_name, pk: userPk, swap_request }) => {
|
||||
const storeUser = store.userStore.items[userPk];
|
||||
|
||||
const { schedule, shift } = event;
|
||||
|
||||
const isCurrentUserSlot = userPk === store.userStore.currentUserPk;
|
||||
const inactive = filters && filters.users.length && !filters.users.includes(userPk);
|
||||
|
||||
const userTitle = storeUser ? getTitle(storeUser) : display_name;
|
||||
const userTitle = showScheduleNameAsSlotTitle ? schedule?.name : storeUser ? getTitle(storeUser) : display_name;
|
||||
|
||||
const isShiftSwap = Boolean(swap_request);
|
||||
|
||||
const title = isShiftSwap ? 'Shift swap' : showScheduleNameAsSlotTitle ? schedule?.name : getShiftName(shift);
|
||||
|
||||
let backgroundColor = color;
|
||||
if (isShiftSwap) {
|
||||
backgroundColor = SHIFT_SWAP_COLOR;
|
||||
|
|
@ -282,7 +287,7 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
key={userPk}
|
||||
content={
|
||||
<ScheduleSlotDetails
|
||||
showScheduleNameAsSlotTitle={showScheduleNameAsSlotTitle}
|
||||
title={title}
|
||||
isShiftSwap={isShiftSwap}
|
||||
beneficiaryName={
|
||||
isShiftSwap ? (swap_request.user ? swap_request.user.display_name : display_name) : undefined
|
||||
|
|
@ -328,7 +333,7 @@ interface ScheduleSlotDetailsProps {
|
|||
beneficiaryName?: string;
|
||||
benefactorName?: string;
|
||||
currentMoment: dayjs.Dayjs;
|
||||
showScheduleNameAsSlotTitle?: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
||||
|
|
@ -344,7 +349,7 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
beneficiaryName,
|
||||
benefactorName,
|
||||
currentMoment,
|
||||
showScheduleNameAsSlotTitle,
|
||||
title,
|
||||
} = props;
|
||||
|
||||
const { scheduleStore } = useStore();
|
||||
|
|
@ -368,8 +373,6 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
}
|
||||
}, [shift]);
|
||||
|
||||
const title = isShiftSwap ? 'Shift swap' : showScheduleNameAsSlotTitle ? schedule?.name : getShiftName(shift);
|
||||
|
||||
// const onCallNow = schedule?.on_call_now;
|
||||
// const isOncall = Boolean(storeUser && onCallNow && onCallNow.some((onCallUser) => storeUser.pk === onCallUser.pk));
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const fillGaps = (events: Event[]) => {
|
|||
return newEvents;
|
||||
};
|
||||
|
||||
export const splitToShiftsAndFillGaps = (events: Event[]) => {
|
||||
export const splitToShifts = (events: Event[]) => {
|
||||
const shifts: Array<{ shiftId: Shift['id']; priority: Shift['priority_level']; events: Event[] }> = [];
|
||||
|
||||
for (const [_i, event] of events.entries()) {
|
||||
|
|
@ -77,13 +77,20 @@ export const splitToShiftsAndFillGaps = (events: Event[]) => {
|
|||
}
|
||||
}
|
||||
|
||||
shifts.forEach((shift) => {
|
||||
shift.events = fillGaps(shift.events);
|
||||
});
|
||||
|
||||
return shifts;
|
||||
};
|
||||
|
||||
export const fillGapsInShifts = (shifts: ShiftEvents[]) => {
|
||||
return shifts.map((shift) => ({
|
||||
...shift,
|
||||
events: fillGaps(shift.events),
|
||||
}));
|
||||
};
|
||||
|
||||
export const enrichEventsWithScheduleData = (events: Event[], schedule: Partial<Schedule>) => {
|
||||
return events.map((event) => ({ ...event, schedule }));
|
||||
};
|
||||
|
||||
export const getPersonalShiftsFromStore = (
|
||||
store: RootStore,
|
||||
userPk: User['pk'],
|
||||
|
|
@ -102,6 +109,44 @@ export const getShiftsFromStore = (
|
|||
: (store.scheduleStore.events[scheduleId]?.['final']?.[getFromString(startMoment)] as any);
|
||||
};
|
||||
|
||||
export const unFlattenShiftEvents = (shifts: ShiftEvents[]) => {
|
||||
for (let i = 0; i < shifts.length; i++) {
|
||||
const shift = shifts[i];
|
||||
|
||||
for (let j = 0; j < shift.events.length - 1; j++) {
|
||||
for (let k = j + 1; k < shift.events.length; k++) {
|
||||
const event1 = shift.events[j];
|
||||
const event2 = shift.events[k];
|
||||
|
||||
const event1Start = dayjs(event1.start);
|
||||
const event1End = dayjs(event1.end);
|
||||
|
||||
const event2Start = dayjs(event2.start);
|
||||
const event2End = dayjs(event2.end);
|
||||
|
||||
if (
|
||||
(event1Start.isBefore(event2Start) && event1End.isAfter(event2Start)) ||
|
||||
(event1End.isAfter(event2End) && event1Start.isBefore(event2End))
|
||||
) {
|
||||
const firstEvent = event1Start.isBefore(event2Start) ? event1 : event2;
|
||||
const secondEvent = firstEvent === event1 ? event2 : event1;
|
||||
|
||||
const oldShift = { ...shift, events: shift.events.filter((event) => event !== secondEvent) };
|
||||
|
||||
const newShift = { ...shift, events: [secondEvent] };
|
||||
|
||||
shifts[i] = oldShift;
|
||||
shifts.push(newShift);
|
||||
|
||||
return unFlattenShiftEvents(shifts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return shifts;
|
||||
};
|
||||
|
||||
export const flattenShiftEvents = (shifts: ShiftEvents[]) => {
|
||||
if (!shifts) {
|
||||
return undefined;
|
||||
|
|
@ -241,9 +286,7 @@ export const getOverridesFromStore = (
|
|||
: (store.scheduleStore.events[scheduleId]?.['override']?.[getFromString(startMoment)] as ShiftEvents[]);
|
||||
};
|
||||
|
||||
export const splitToLayers = (
|
||||
shifts: Array<{ shiftId: Shift['id']; priority: Shift['priority_level']; events: Event[] }>
|
||||
) => {
|
||||
export const splitToLayers = (shifts: ShiftEvents[]) => {
|
||||
return shifts
|
||||
.reduce((memo, shift) => {
|
||||
let layer = memo.find((level) => level.priority === shift.priority);
|
||||
|
|
@ -395,7 +438,7 @@ export const getOverrideColor = (rotationIndex: number) => {
|
|||
return OVERRIDE_COLORS[normalizedRotationIndex];
|
||||
};
|
||||
|
||||
export const getShiftName = (shift: Shift) => {
|
||||
export const getShiftName = (shift: Partial<Shift>) => {
|
||||
if (!shift) {
|
||||
return '';
|
||||
}
|
||||
|
|
@ -408,5 +451,5 @@ export const getShiftName = (shift: Shift) => {
|
|||
return 'Override';
|
||||
}
|
||||
|
||||
return `[L${shift.priority_level}] Rotation`;
|
||||
return 'Rotation';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ import { SelectOption } from 'state/types';
|
|||
|
||||
import {
|
||||
createShiftSwapEventFromShiftSwap,
|
||||
enrichEventsWithScheduleData,
|
||||
enrichLayers,
|
||||
enrichOverrides,
|
||||
fillGapsInShifts,
|
||||
flattenShiftEvents,
|
||||
getFromString,
|
||||
splitToLayers,
|
||||
splitToShiftsAndFillGaps,
|
||||
splitToShifts,
|
||||
unFlattenShiftEvents,
|
||||
} from './schedule.helpers';
|
||||
import {
|
||||
Rotation,
|
||||
|
|
@ -34,7 +37,7 @@ import {
|
|||
|
||||
export class ScheduleStore extends BaseStore {
|
||||
@observable
|
||||
searchResult: { count?: number; results?: Array<Schedule['id']> } = {};
|
||||
searchResult: { page_size?: number; count?: number; results?: Array<Schedule['id']> } = {};
|
||||
|
||||
@observable.shallow
|
||||
items: { [id: string]: Schedule } = {};
|
||||
|
|
@ -137,7 +140,7 @@ export class ScheduleStore extends BaseStore {
|
|||
shouldUpdateFn: () => boolean = undefined
|
||||
) {
|
||||
const filters = typeof f === 'string' ? { search: f } : f;
|
||||
const { count, results } = await makeRequest(this.path, {
|
||||
const { page_size, count, results } = await makeRequest(this.path, {
|
||||
method: 'GET',
|
||||
params: { ...filters, page },
|
||||
});
|
||||
|
|
@ -157,6 +160,7 @@ export class ScheduleStore extends BaseStore {
|
|||
),
|
||||
};
|
||||
this.searchResult = {
|
||||
page_size,
|
||||
count,
|
||||
results: results.map((item: Schedule) => item.id),
|
||||
};
|
||||
|
|
@ -193,6 +197,7 @@ export class ScheduleStore extends BaseStore {
|
|||
return undefined;
|
||||
}
|
||||
return {
|
||||
page_size: this.searchResult.page_size,
|
||||
count: this.searchResult.count,
|
||||
results: this.searchResult.results?.map((scheduleId: Schedule['id']) => this.items[scheduleId]),
|
||||
};
|
||||
|
|
@ -287,7 +292,7 @@ export class ScheduleStore extends BaseStore {
|
|||
this.rotationPreview = { ...this.rotationPreview, [fromString]: layers };
|
||||
}
|
||||
|
||||
this.finalPreview = { ...this.finalPreview, [fromString]: splitToShiftsAndFillGaps(response.final) };
|
||||
this.finalPreview = { ...this.finalPreview, [fromString]: fillGapsInShifts(splitToShifts(response.final)) };
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -450,7 +455,9 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
|
||||
const fromString = getFromString(startMoment);
|
||||
const shifts = splitToShiftsAndFillGaps(response.events);
|
||||
const shiftsRaw = splitToShifts(response.events);
|
||||
const shiftsUnflattened = unFlattenShiftEvents(shiftsRaw);
|
||||
const shifts = fillGapsInShifts(shiftsUnflattened);
|
||||
const layers = type === 'rotation' ? splitToLayers(shifts) : undefined;
|
||||
|
||||
this.events = {
|
||||
|
|
@ -535,7 +542,7 @@ export class ScheduleStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9) {
|
||||
async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9, isUpdateOnCallNow = false) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
||||
const dayBefore = startMoment.subtract(1, 'day');
|
||||
|
|
@ -548,8 +555,8 @@ export class ScheduleStore extends BaseStore {
|
|||
},
|
||||
});
|
||||
|
||||
const shiftEventsList = schedules.reduce((acc, schedule) => {
|
||||
return [...acc, ...splitToShiftsAndFillGaps(schedule.events)];
|
||||
const shiftEventsList = schedules.reduce((acc, { events, id, name }) => {
|
||||
return [...acc, ...fillGapsInShifts(splitToShifts(enrichEventsWithScheduleData(events, { id, name })))];
|
||||
}, []);
|
||||
|
||||
const shiftEventsListFlattened = flattenShiftEvents(shiftEventsList);
|
||||
|
|
@ -562,9 +569,12 @@ export class ScheduleStore extends BaseStore {
|
|||
},
|
||||
};
|
||||
|
||||
this.onCallNow = {
|
||||
...this.onCallNow,
|
||||
[userPk]: is_oncall,
|
||||
};
|
||||
if (isUpdateOnCallNow) {
|
||||
// since current endpoint works incorrectly we are waiting for https://github.com/grafana/oncall/issues/3164
|
||||
this.onCallNow = {
|
||||
...this.onCallNow,
|
||||
[userPk]: is_oncall,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export interface Event {
|
|||
is_gap: boolean;
|
||||
missing_users: Array<{ display_name: User['username']; pk: User['pk'] }>;
|
||||
priority_level: number;
|
||||
shift: { pk: Shift['id'] | null };
|
||||
shift: Pick<Shift, 'name' | 'type'> & { pk: string };
|
||||
source: string;
|
||||
start: string;
|
||||
users: Array<{
|
||||
|
|
@ -104,6 +104,7 @@ export interface Event {
|
|||
}>;
|
||||
is_override: boolean;
|
||||
|
||||
schedule?: Partial<Schedule>; // populated by frontend for personal schedule to display schedule name instead of user name
|
||||
shiftSwapId?: ShiftSwap['id']; // if event is acually shift swap request (filled out by frontend)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
shiftIdToShowRotationForm ||
|
||||
shiftSwapIdToShowForm;
|
||||
|
||||
const disabledShiftSwaps =
|
||||
!isUserActionAllowed(UserActions.SchedulesWrite) ||
|
||||
!!shiftIdToShowOverridesForm ||
|
||||
shiftIdToShowRotationForm ||
|
||||
shiftSwapIdToShowForm;
|
||||
|
||||
return (
|
||||
<PageErrorHandlingWrapper errorData={errorData} objectName="schedule" pageName="schedules">
|
||||
{() => (
|
||||
|
|
@ -314,6 +320,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
|
||||
onShowRotationForm={this.handleShowOverridesForm}
|
||||
disabled={disabledOverrideForm}
|
||||
disableShiftSwaps={disabledShiftSwaps}
|
||||
shiftStartToShowOverrideForm={shiftStartToShowOverrideForm}
|
||||
shiftEndToShowOverrideForm={shiftEndToShowOverrideForm}
|
||||
onShowShiftSwapForm={!shiftSwapIdToShowForm ? this.handleShowShiftSwapForm : undefined}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import SchedulePersonal from 'containers/Rotations/SchedulePersonal';
|
|||
import ScheduleForm from 'containers/ScheduleForm/ScheduleForm';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { Schedule, ScheduleType } from 'models/schedule/schedule.types';
|
||||
import { Schedule } from 'models/schedule/schedule.types';
|
||||
import { getSlackChannelName } from 'models/slack_channel/slack_channel.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
|
||||
|
|
@ -39,7 +39,7 @@ import styles from './Schedules.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
const FILTERS_DEBOUNCE_MS = 500;
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
const PAGE_SIZE_DEFAULT = 15;
|
||||
|
||||
interface SchedulesPageProps extends WithStoreProps, RouteComponentProps, PageProps {}
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
const { grafanaTeamStore } = store;
|
||||
const { showNewScheduleSelector, expandedRowKeys, scheduleIdToEdit, page, startMoment } = this.state;
|
||||
|
||||
const { results, count } = store.scheduleStore.getSearchResult();
|
||||
const { results, count, page_size } = store.scheduleStore.getSearchResult();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
|
@ -162,9 +162,6 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
userPk={store.userStore.currentUserPk}
|
||||
currentTimezone={store.currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onSlotClick={(...rest) => {
|
||||
console.log(rest);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={cx('schedules__filters-container')}>
|
||||
|
|
@ -179,7 +176,11 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
columns={columns}
|
||||
data={results}
|
||||
loading={!results}
|
||||
pagination={{ page, total: Math.ceil((count || 0) / ITEMS_PER_PAGE), onChange: this.handlePageChange }}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / (page_size || PAGE_SIZE_DEFAULT)),
|
||||
onChange: this.handlePageChange,
|
||||
}}
|
||||
rowKey="id"
|
||||
expandable={{
|
||||
expandedRowKeys: expandedRowKeys,
|
||||
|
|
@ -234,9 +235,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
handleCreateSchedule = (data: Schedule) => {
|
||||
const { history, query } = this.props;
|
||||
|
||||
if (data.type === ScheduleType.API) {
|
||||
history.push(`${PLUGIN_ROOT}/schedules/${data.id}?${qs.stringify(query)}`);
|
||||
}
|
||||
history.push(`${PLUGIN_ROOT}/schedules/${data.id}?${qs.stringify(query)}`);
|
||||
};
|
||||
|
||||
handleExpandRow = (expanded: boolean, data: Schedule) => {
|
||||
|
|
@ -455,7 +454,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
const { store } = this.props;
|
||||
const { page, startMoment } = this.state;
|
||||
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment);
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment, 9, true);
|
||||
|
||||
// For removal we need to check if count is 1
|
||||
// which means we should change the page to the previous one
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue