Brojd/fix timezone issues (#3618)
# What this PR does - fix multiple issues on schedule related to handling timezones ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/3576 ## 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] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
5337baa0fc
commit
82b5a877d9
37 changed files with 824 additions and 888 deletions
|
|
@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Move Insights to OnCall as a separate page ([#2382](https://github.com/grafana/oncall-private/issues/2382))
|
||||
- Allow mobile app to access paging endpoint @imtoori ([#3619](https://github.com/grafana/oncall/pull/3619))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed schedule timezone issues ([#3576](https://github.com/grafana/oncall/issues/3576))
|
||||
|
||||
## v1.3.82 (2024-01-04)
|
||||
|
||||
### Added
|
||||
|
|
|
|||
55
grafana-plugin/e2e-tests/schedules/timezones.test.ts
Normal file
55
grafana-plugin/e2e-tests/schedules/timezones.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
import { test } from '../fixtures';
|
||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
test.use({ timezoneId: 'Europe/Moscow' }); // GMT+3 the whole year
|
||||
const currentUtcTime = dayjs().utc().format('HH:mm');
|
||||
const currentUtcDate = dayjs().utc().format('DD MMM');
|
||||
const currentMoscowTime = dayjs().utcOffset(180).format('HH:mm');
|
||||
const currentMoscowDate = dayjs().utcOffset(180).format('DD MMM');
|
||||
|
||||
test('default dates in override creation modal are correct', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
|
||||
// Current timezone is selected by default to currently logged in user timezone
|
||||
await expect(page.getByTestId('timezone-select')).toHaveText('GMT+3');
|
||||
|
||||
// Change timezone to GMT
|
||||
await page.getByTestId('timezone-select').getByRole('img').click();
|
||||
await page.getByText('GMT', { exact: true }).click();
|
||||
|
||||
// Selected timezone and local time is correctly displayed
|
||||
await expect(page.getByText(`Current timezone: GMT, local time: ${currentUtcTime}`)).toBeVisible();
|
||||
|
||||
// // User avatar tooltip shows correct time and timezones
|
||||
await page.getByTestId('user-avatar-in-schedule').hover();
|
||||
await expect(page.getByTestId('schedule-user-details_your-current-time')).toHaveText(/GMT\+3/);
|
||||
await expect(page.getByTestId('schedule-user-details_your-current-time')).toHaveText(new RegExp(currentMoscowTime));
|
||||
await expect(page.getByTestId('schedule-user-details_user-local-time')).toHaveText(/GMT\+3/);
|
||||
await expect(page.getByTestId('schedule-user-details_user-local-time')).toHaveText(new RegExp(currentMoscowTime));
|
||||
|
||||
// Schedule slot shows correct times and timezones
|
||||
await page.getByTestId('schedule-slot').first().hover();
|
||||
await expect(page.getByText(`User's local time${currentMoscowDate}, ${currentMoscowTime}(GMT+3)`)).toBeVisible();
|
||||
await expect(page.getByText(`Current timezone${currentUtcDate}, ${currentUtcTime}(GMT)`)).toBeVisible();
|
||||
|
||||
// Rotation form has correct start date and current timezone information
|
||||
await clickButton({ page, buttonText: 'Add rotation' });
|
||||
await page.getByText('Layer 1 rotation').click();
|
||||
await expect(page.getByTestId('rotation-form').getByText('Current timezone: GMT')).toBeVisible();
|
||||
await expect(page.getByTestId('rotation-form').getByPlaceholder('Date')).toHaveValue(
|
||||
dayjs().utcOffset(0).format('MM/DD/YYYY')
|
||||
);
|
||||
await expect(page.getByTestId('rotation-form').getByTestId('date-time-picker').getByRole('textbox')).toHaveValue(
|
||||
'00:00'
|
||||
);
|
||||
});
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import 'plugin/dayjs';
|
||||
|
||||
// https://stackoverflow.com/a/66055672
|
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import React, { FC, useEffect, useState } from 'react';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
|
||||
import { Tooltip, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import { ScheduleQualityDetails } from 'components/ScheduleQualityDetails/ScheduleQualityDetails';
|
||||
import Tag from 'components/Tag/Tag';
|
||||
import Text from 'components/Text/Text';
|
||||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
import { Schedule, ScheduleScoreQualityResponse, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
||||
import { Schedule, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './ScheduleQuality.module.scss';
|
||||
|
|
@ -17,29 +18,29 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface ScheduleQualityProps {
|
||||
schedule: Schedule;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
const ScheduleQuality: FC<ScheduleQualityProps> = ({ schedule, lastUpdated }) => {
|
||||
const { scheduleStore } = useStore();
|
||||
const [qualityResponse, setQualityResponse] = useState<ScheduleScoreQualityResponse>(undefined);
|
||||
const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule }) => {
|
||||
const {
|
||||
scheduleStore: { getScoreQuality, relatedEscalationChains, quality },
|
||||
} = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (schedule.id) {
|
||||
fetchScoreQuality();
|
||||
getScoreQuality(schedule.id);
|
||||
}
|
||||
}, [schedule.id, lastUpdated]);
|
||||
}, [schedule.id]);
|
||||
|
||||
if (!qualityResponse) {
|
||||
if (!quality) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relatedEscalationChains = scheduleStore.relatedEscalationChains[schedule.id];
|
||||
const relatedScheduleEscalationChains = relatedEscalationChains[schedule.id];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')} data-testid="schedule-quality">
|
||||
{relatedEscalationChains?.length > 0 && schedule?.number_of_escalation_chains > 0 && (
|
||||
{relatedScheduleEscalationChains?.length > 0 && schedule?.number_of_escalation_chains > 0 && (
|
||||
<TooltipBadge
|
||||
borderType="success"
|
||||
icon="link"
|
||||
|
|
@ -48,7 +49,7 @@ const ScheduleQuality: FC<ScheduleQualityProps> = ({ schedule, lastUpdated }) =>
|
|||
tooltipTitle="Used in escalations"
|
||||
tooltipContent={
|
||||
<VerticalGroup spacing="sm">
|
||||
{relatedEscalationChains.map((escalationChain) => (
|
||||
{relatedScheduleEscalationChains.map((escalationChain) => (
|
||||
<div key={escalationChain.pk}>
|
||||
<PluginLink query={{ page: 'escalations', id: escalationChain.pk }} className="link">
|
||||
<Text type="link">{escalationChain.name}</Text>
|
||||
|
|
@ -82,13 +83,11 @@ const ScheduleQuality: FC<ScheduleQualityProps> = ({ schedule, lastUpdated }) =>
|
|||
<Tooltip
|
||||
placement="bottom-start"
|
||||
interactive
|
||||
content={
|
||||
<ScheduleQualityDetails quality={qualityResponse} getScheduleQualityString={getScheduleQualityString} />
|
||||
}
|
||||
content={<ScheduleQualityDetails quality={quality} getScheduleQualityString={getScheduleQualityString} />}
|
||||
>
|
||||
<div className={cx('u-cursor-default')}>
|
||||
<Tag className={cx('tag', getTagClass())}>
|
||||
Quality: <strong>{getScheduleQualityString(qualityResponse.total_score)}</strong>
|
||||
Quality: <strong>{getScheduleQualityString(quality.total_score)}</strong>
|
||||
</Tag>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
|
@ -112,22 +111,15 @@ const ScheduleQuality: FC<ScheduleQualityProps> = ({ schedule, lastUpdated }) =>
|
|||
return ScheduleScoreQualityResult.Great;
|
||||
}
|
||||
|
||||
async function fetchScoreQuality() {
|
||||
await Promise.all([
|
||||
scheduleStore.getScoreQuality(schedule.id).then((qualityResponse) => setQualityResponse(qualityResponse)),
|
||||
scheduleStore.updateRelatedEscalationChains(schedule.id),
|
||||
]);
|
||||
}
|
||||
|
||||
function getTagClass() {
|
||||
if (qualityResponse?.total_score < 20) {
|
||||
if (quality?.total_score < 20) {
|
||||
return 'tag--danger';
|
||||
}
|
||||
if (qualityResponse?.total_score < 60) {
|
||||
if (quality?.total_score < 60) {
|
||||
return 'tag--warning';
|
||||
}
|
||||
return 'tag--primary';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default ScheduleQuality;
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import React, { FC, useMemo, useState } from 'react';
|
|||
import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
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 { Timezone } from 'models/timezone/timezone.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import RotationTutorial from './RotationTutorial';
|
||||
|
||||
|
|
@ -18,8 +19,6 @@ import styles from './Rotation.module.css';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface RotationProps {
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
layerIndex?: number;
|
||||
rotationIndex?: number;
|
||||
color?: string;
|
||||
|
|
@ -40,11 +39,12 @@ interface RotationProps {
|
|||
showScheduleNameAsSlotTitle?: boolean;
|
||||
}
|
||||
|
||||
const Rotation: FC<RotationProps> = (props) => {
|
||||
const Rotation: FC<RotationProps> = observer((props) => {
|
||||
const {
|
||||
timezoneStore: { calendarStartDate },
|
||||
} = useStore();
|
||||
const {
|
||||
events,
|
||||
startMoment,
|
||||
currentTimezone,
|
||||
color: propsColor,
|
||||
days = 7,
|
||||
transparent = false,
|
||||
|
|
@ -71,7 +71,7 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
|
||||
const dayOffset = Math.floor((x / width) * 7);
|
||||
|
||||
const shiftStart = startMoment.add(dayOffset, 'day');
|
||||
const shiftStart = calendarStartDate.add(dayOffset, 'day');
|
||||
const shiftEnd = shiftStart.add(1, 'day');
|
||||
|
||||
onClick(shiftStart, shiftEnd);
|
||||
|
|
@ -133,7 +133,7 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
}
|
||||
|
||||
const firstShift = events[0];
|
||||
const firstShiftOffset = dayjs(firstShift.start).diff(startMoment, 'seconds');
|
||||
const firstShiftOffset = dayjs(firstShift.start).diff(calendarStartDate, 'seconds');
|
||||
const base = 60 * 60 * 24 * days;
|
||||
|
||||
return firstShiftOffset / base;
|
||||
|
|
@ -142,7 +142,7 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
return (
|
||||
<div className={cx('root')} onClick={onClick && handleRotationClick}>
|
||||
<div className={cx('timeline')}>
|
||||
{tutorialParams && <RotationTutorial startMoment={startMoment} {...tutorialParams} />}
|
||||
{tutorialParams && <RotationTutorial {...tutorialParams} />}
|
||||
{events ? (
|
||||
events.length ? (
|
||||
<div
|
||||
|
|
@ -154,8 +154,6 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
<ScheduleSlot
|
||||
key={hash(event)}
|
||||
event={event}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
color={propsColor || getColor(event)}
|
||||
handleAddOverride={getAddOverrideClickHandler(event)}
|
||||
handleAddShiftSwap={getAddShiftSwapClickHandler(event)}
|
||||
|
|
@ -179,7 +177,7 @@ const Rotation: FC<RotationProps> = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const Empty = ({ text }: { text: string }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,20 +2,24 @@ import React, { FC, useMemo } from 'react';
|
|||
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { RotationFormLiveParams } from 'models/schedule/schedule.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './Rotation.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface RotationProps extends RotationFormLiveParams {
|
||||
startMoment: dayjs.Dayjs;
|
||||
days?: number;
|
||||
}
|
||||
|
||||
const RotationTutorial: FC<RotationProps> = (props) => {
|
||||
const { startMoment, days = 7, shiftStart, shiftEnd, rotationStart, focusElementName } = props;
|
||||
const RotationTutorial: FC<RotationProps> = observer((props) => {
|
||||
const {
|
||||
timezoneStore: { calendarStartDate },
|
||||
} = useStore();
|
||||
const { days = 7, shiftStart, shiftEnd, rotationStart, focusElementName } = props;
|
||||
|
||||
const duration = shiftEnd.diff(shiftStart, 'seconds');
|
||||
|
||||
|
|
@ -48,11 +52,11 @@ const RotationTutorial: FC<RotationProps> = (props) => {
|
|||
}
|
||||
|
||||
const firstEvent = events[0];
|
||||
const firstShiftOffset = dayjs(firstEvent.start).diff(startMoment, 'seconds');
|
||||
const firstShiftOffset = dayjs(firstEvent.start).diff(calendarStartDate, 'seconds');
|
||||
const base = 60 * 60 * 24 * days;
|
||||
|
||||
return firstShiftOffset / base;
|
||||
}, [events, startMoment]);
|
||||
}, [events, calendarStartDate]);
|
||||
|
||||
return (
|
||||
<div className={cx('slots', 'slots--tutorial')} style={{ transform: `translate(${x * 100}%, 0)` }}>
|
||||
|
|
@ -73,7 +77,7 @@ const RotationTutorial: FC<RotationProps> = (props) => {
|
|||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const TutorialSlot = (props: { style: React.CSSProperties; active: boolean }) => {
|
||||
const { style, active } = props;
|
||||
|
|
|
|||
|
|
@ -47,13 +47,11 @@ import TimeUnitSelector from 'containers/RotationForm/parts/TimeUnitSelector';
|
|||
import UserItem from 'containers/RotationForm/parts/UserItem';
|
||||
import { getShiftName } from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift } from 'models/schedule/schedule.types';
|
||||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import {
|
||||
getDateTime,
|
||||
getSelectedDays,
|
||||
getStartOfWeek,
|
||||
getStartOfWeekBasedOnCurrentDate,
|
||||
getUTCByDay,
|
||||
getUTCString,
|
||||
getUTCWeekStart,
|
||||
|
|
@ -71,8 +69,6 @@ const cx = cn.bind(styles);
|
|||
interface RotationFormProps {
|
||||
layerPriority: number;
|
||||
onHide: () => void;
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
scheduleId: Schedule['id'];
|
||||
shiftId: Shift['id'] | 'new';
|
||||
shiftStart?: dayjs.Dayjs;
|
||||
|
|
@ -85,23 +81,21 @@ interface RotationFormProps {
|
|||
}
|
||||
|
||||
const RotationForm = observer((props: RotationFormProps) => {
|
||||
const store = useStore();
|
||||
const {
|
||||
onHide,
|
||||
onCreate,
|
||||
startMoment,
|
||||
currentTimezone,
|
||||
scheduleId,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
layerPriority,
|
||||
shiftId,
|
||||
shiftStart: propsShiftStart = getStartOfWeek(currentTimezone),
|
||||
shiftStart: propsShiftStart = getStartOfWeekBasedOnCurrentDate(store.timezoneStore.currentDateInSelectedTimezone),
|
||||
shiftEnd: propsShiftEnd,
|
||||
shiftColor = '#3D71D9',
|
||||
onShowRotationForm,
|
||||
} = props;
|
||||
|
||||
const store = useStore();
|
||||
const shift = store.scheduleStore.shifts[shiftId];
|
||||
|
||||
const [errors, setErrors] = useState<{ [key: string]: string[] }>({});
|
||||
|
|
@ -192,7 +186,7 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
setErrors({});
|
||||
|
||||
store.scheduleStore
|
||||
.updateRotationPreview(scheduleId, shiftId, startMoment, false, params)
|
||||
.updateRotationPreview(scheduleId, shiftId, store.timezoneStore.calendarStartDate, false, params)
|
||||
.catch(onError)
|
||||
.finally(() => {
|
||||
setIsOpen(true);
|
||||
|
|
@ -214,14 +208,20 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
rolling_users: userGroups,
|
||||
interval: repeatEveryValue,
|
||||
frequency: repeatEveryPeriod,
|
||||
by_day: getUTCByDay(store.scheduleStore.byDayOptions, selectedDays, shiftStart.tz(currentTimezone)),
|
||||
week_start: getUTCWeekStart(store.scheduleStore.byDayOptions, shiftStart.tz(currentTimezone)),
|
||||
by_day: getUTCByDay(
|
||||
store.scheduleStore.byDayOptions,
|
||||
selectedDays,
|
||||
store.timezoneStore.getDateInSelectedTimezone(shiftStart)
|
||||
),
|
||||
week_start: getUTCWeekStart(
|
||||
store.scheduleStore.byDayOptions,
|
||||
store.timezoneStore.getDateInSelectedTimezone(shiftStart)
|
||||
),
|
||||
priority_level: shiftId === 'new' ? layerPriority : shift?.priority_level,
|
||||
name: rotationName,
|
||||
}),
|
||||
[
|
||||
rotationStart,
|
||||
currentTimezone,
|
||||
rotationEnd,
|
||||
shiftStart,
|
||||
shiftEnd,
|
||||
|
|
@ -237,7 +237,7 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
]
|
||||
);
|
||||
|
||||
useEffect(handleChange, [params, startMoment]);
|
||||
useEffect(handleChange, [params, store.timezoneStore.calendarStartDate]);
|
||||
|
||||
const create = useCallback(() => {
|
||||
store.scheduleStore
|
||||
|
|
@ -383,7 +383,13 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
|
||||
setRepeatEveryValue(shift.interval);
|
||||
setRepeatEveryPeriod(shift.frequency);
|
||||
setSelectedDays(getSelectedDays(store.scheduleStore.byDayOptions, shift.by_day, shiftStart.tz(currentTimezone)));
|
||||
setSelectedDays(
|
||||
getSelectedDays(
|
||||
store.scheduleStore.byDayOptions,
|
||||
shift.by_day,
|
||||
store.timezoneStore.getDateInSelectedTimezone(shiftStart)
|
||||
)
|
||||
);
|
||||
|
||||
setShowActiveOnSelectedDays(Boolean(shift.by_day?.length));
|
||||
|
||||
|
|
@ -405,9 +411,15 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (shift) {
|
||||
setSelectedDays(getSelectedDays(store.scheduleStore.byDayOptions, shift.by_day, shiftStart.tz(currentTimezone)));
|
||||
setSelectedDays(
|
||||
getSelectedDays(
|
||||
store.scheduleStore.byDayOptions,
|
||||
shift.by_day,
|
||||
store.timezoneStore.getDateInSelectedTimezone(shiftStart)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [currentTimezone]);
|
||||
}, [store.timezoneStore.selectedTimezoneOffset]);
|
||||
|
||||
const isFormValid = useMemo(() => !Object.keys(errors).length, [errors]);
|
||||
|
||||
|
|
@ -435,7 +447,7 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
</Draggable>
|
||||
)}
|
||||
>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('root')} data-testid="rotation-form">
|
||||
<div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="sm">
|
||||
|
|
@ -501,10 +513,8 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
}
|
||||
>
|
||||
<DateTimePicker
|
||||
//minMoment={shiftStart}
|
||||
value={rotationStart}
|
||||
onChange={handleRotationStartChange}
|
||||
timezone={currentTimezone}
|
||||
error={errors.rotation_start}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
@ -533,7 +543,6 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
<DateTimePicker
|
||||
value={rotationEnd}
|
||||
onChange={setRotationEnd}
|
||||
timezone={currentTimezone}
|
||||
error={errors.until}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
@ -617,7 +626,6 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
defaultValue={shiftPeriodDefaultValue}
|
||||
shiftStart={shiftStart}
|
||||
onChange={handleActivePeriodChange}
|
||||
currentTimezone={currentTimezone}
|
||||
disabled={disabled}
|
||||
errors={errors}
|
||||
/>
|
||||
|
|
@ -660,7 +668,7 @@ const RotationForm = observer((props: RotationFormProps) => {
|
|||
</div>
|
||||
<div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text type="secondary">Current timezone: {getTzOffsetString(dayjs().tz(currentTimezone))}</Text>
|
||||
<Text type="secondary">Current timezone: {store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
<HorizontalGroup>
|
||||
{shiftId !== 'new' && (
|
||||
<Tooltip content="Stop the current rotation and start a new one">
|
||||
|
|
@ -712,7 +720,6 @@ interface ShiftPeriodProps {
|
|||
defaultValue: number;
|
||||
shiftStart: dayjs.Dayjs;
|
||||
onChange: (value: number) => void;
|
||||
currentTimezone: Timezone;
|
||||
disabled: boolean;
|
||||
errors: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import UserGroups from 'components/UserGroups/UserGroups';
|
|||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { getShiftName } from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift } from 'models/schedule/schedule.types';
|
||||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -29,8 +27,6 @@ import styles from './RotationForm.module.css';
|
|||
interface RotationFormProps {
|
||||
onHide: () => void;
|
||||
shiftId: Shift['id'] | 'new';
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
scheduleId: Schedule['id'];
|
||||
shiftStart?: dayjs.Dayjs;
|
||||
shiftEnd?: dayjs.Dayjs;
|
||||
|
|
@ -46,12 +42,10 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
const {
|
||||
onHide,
|
||||
onCreate,
|
||||
currentTimezone,
|
||||
scheduleId,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
shiftId,
|
||||
startMoment,
|
||||
shiftStart: propsShiftStart = dayjs().startOf('day').add(1, 'day'),
|
||||
shiftEnd: propsShiftEnd,
|
||||
shiftColor = getVar('--tag-warning'),
|
||||
|
|
@ -116,7 +110,7 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
frequency: null,
|
||||
name: rotationName,
|
||||
}),
|
||||
[currentTimezone, shiftStart, shiftEnd, userGroups, rotationName]
|
||||
[shiftStart, shiftEnd, userGroups, rotationName, store.timezoneStore.selectedTimezoneOffset]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -172,7 +166,7 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
setErrors({});
|
||||
|
||||
store.scheduleStore
|
||||
.updateRotationPreview(scheduleId, shiftId, startMoment, true, params)
|
||||
.updateRotationPreview(scheduleId, shiftId, store.timezoneStore.calendarStartDate, true, params)
|
||||
.catch(onError)
|
||||
.finally(() => {
|
||||
setIsOpen(true);
|
||||
|
|
@ -185,7 +179,7 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
|
||||
const handleChange = useDebouncedCallback(updatePreview, 200);
|
||||
|
||||
useEffect(handleChange, [params, startMoment]);
|
||||
useEffect(handleChange, [params, store.timezoneStore.calendarStartDate]);
|
||||
|
||||
const isFormValid = useMemo(() => !Object.keys(errors).length, [errors]);
|
||||
|
||||
|
|
@ -242,7 +236,6 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
disabled={disabled}
|
||||
value={shiftStart}
|
||||
onChange={updateShiftStart}
|
||||
timezone={currentTimezone}
|
||||
error={errors.shift_start}
|
||||
/>
|
||||
</Field>
|
||||
|
|
@ -254,13 +247,7 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
</Text>
|
||||
}
|
||||
>
|
||||
<DateTimePicker
|
||||
disabled={disabled}
|
||||
value={shiftEnd}
|
||||
onChange={setShiftEnd}
|
||||
timezone={currentTimezone}
|
||||
error={errors.shift_end}
|
||||
/>
|
||||
<DateTimePicker disabled={disabled} value={shiftEnd} onChange={setShiftEnd} error={errors.shift_end} />
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
<UserGroups
|
||||
|
|
@ -276,7 +263,7 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
</VerticalGroup>
|
||||
</div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text type="secondary">Current timezone: {getTzOffsetString(dayjs().tz(currentTimezone))}</Text>
|
||||
<Text type="secondary">Current timezone: {store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
<HorizontalGroup>
|
||||
<Button variant="primary" onClick={handleCreate} disabled={disabled || !isFormValid}>
|
||||
{shiftId === 'new' ? 'Create' : 'Update'}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import WithConfirm from 'components/WithConfirm/WithConfirm';
|
|||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { SHIFT_SWAP_COLOR } from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
|
@ -28,17 +26,13 @@ const cx = cn.bind(styles);
|
|||
interface ShiftSwapFormProps {
|
||||
id: ShiftSwap['id'] | 'new';
|
||||
scheduleId: Schedule['id'];
|
||||
startMoment: dayjs.Dayjs;
|
||||
params: Partial<ShiftSwap>;
|
||||
currentTimezone: Timezone;
|
||||
|
||||
onUpdate: () => void;
|
||||
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
const ShiftSwapForm = (props: ShiftSwapFormProps) => {
|
||||
const { onUpdate, onHide, id, scheduleId, startMoment, params: defaultParams, currentTimezone } = props;
|
||||
const { onUpdate, onHide, id, scheduleId, params: defaultParams } = props;
|
||||
|
||||
const [shiftSwap, setShiftSwap] = useState({ ...defaultParams });
|
||||
|
||||
|
|
@ -84,13 +78,13 @@ const ShiftSwapForm = (props: ShiftSwapFormProps) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (id === 'new') {
|
||||
store.scheduleStore.updateShiftsSwapPreview(scheduleId, startMoment, {
|
||||
store.scheduleStore.updateShiftsSwapPreview(scheduleId, store.timezoneStore.calendarStartDate, {
|
||||
id: 'new',
|
||||
beneficiary: { pk: currentUserPk },
|
||||
...shiftSwap,
|
||||
});
|
||||
}
|
||||
}, [shiftSwap, startMoment]);
|
||||
}, [shiftSwap, store.timezoneStore.calendarStartDate]);
|
||||
|
||||
const handleDescriptionChange = useCallback(
|
||||
(event) => {
|
||||
|
|
@ -173,7 +167,6 @@ const ShiftSwapForm = (props: ShiftSwapFormProps) => {
|
|||
<HorizontalGroup height="auto">
|
||||
<Field label="Swap start">
|
||||
<DateTimePicker
|
||||
timezone={store.currentTimezone}
|
||||
disabled={!isNew}
|
||||
value={dayjs(shiftSwap.swap_start)}
|
||||
onChange={handleShiftSwapStartChange}
|
||||
|
|
@ -181,7 +174,6 @@ const ShiftSwapForm = (props: ShiftSwapFormProps) => {
|
|||
</Field>
|
||||
<Field label="Swap end">
|
||||
<DateTimePicker
|
||||
timezone={store.currentTimezone}
|
||||
disabled={!isNew}
|
||||
value={dayjs(shiftSwap.swap_end)}
|
||||
onChange={handleShiftSwapEndChange}
|
||||
|
|
@ -211,7 +203,7 @@ const ShiftSwapForm = (props: ShiftSwapFormProps) => {
|
|||
</div>
|
||||
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text type="secondary">Current timezone: {getTzOffsetString(dayjs().tz(currentTimezone))}</Text>
|
||||
<Text type="secondary">Current timezone: {store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
<HorizontalGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.SchedulesWrite}>
|
||||
{isNew ? (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { DateTime, dateTime } from '@grafana/data';
|
||||
import { DatePickerWithInput, TimeOfDayPicker, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
import { toDate } from 'containers/RotationForm/RotationForm.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from 'containers/RotationForm/RotationForm.module.css';
|
||||
|
||||
|
|
@ -15,72 +15,85 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface DateTimePickerProps {
|
||||
value: dayjs.Dayjs;
|
||||
timezone: Timezone;
|
||||
onChange: (value: dayjs.Dayjs) => void;
|
||||
disabled?: boolean;
|
||||
minMoment?: dayjs.Dayjs;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
error?: string[];
|
||||
}
|
||||
|
||||
const DateTimePicker = (props: DateTimePickerProps) => {
|
||||
const { value: propValue, minMoment, timezone, onChange, disabled, onFocus, onBlur, error } = props;
|
||||
const DateTimePicker = observer(
|
||||
({ value: propValue, onChange, disabled, onFocus, onBlur, error }: DateTimePickerProps) => {
|
||||
const {
|
||||
timezoneStore: { getDateInSelectedTimezone },
|
||||
} = useStore();
|
||||
const valueInSelectedTimezone = getDateInSelectedTimezone(propValue);
|
||||
const valueAsDate = valueInSelectedTimezone.toDate();
|
||||
|
||||
const value = useMemo(() => toDate(propValue, timezone), [propValue, timezone]);
|
||||
const handleDateChange = (newDate: Date) => {
|
||||
const localMoment = getDateInSelectedTimezone(dayjs(newDate));
|
||||
const newValue = localMoment
|
||||
.set('year', newDate.getFullYear())
|
||||
.set('month', newDate.getMonth())
|
||||
.set('date', newDate.getDate())
|
||||
.set('hour', valueAsDate.getHours())
|
||||
.set('minute', valueAsDate.getMinutes())
|
||||
.set('second', valueAsDate.getSeconds());
|
||||
|
||||
const minDate = useMemo(() => (minMoment ? toDate(minMoment, timezone) : undefined), [minMoment, timezone]);
|
||||
onChange(newValue);
|
||||
};
|
||||
const handleTimeChange = (newMoment: DateTime) => {
|
||||
const selectedHour = newMoment.hour();
|
||||
const selectedMinute = newMoment.minute();
|
||||
const newValue = valueInSelectedTimezone.set('hour', selectedHour).set('minute', selectedMinute);
|
||||
|
||||
const handleDateChange = (newDate: Date) => {
|
||||
const localMoment = dayjs().tz(timezone).utcOffset() === 0 ? dayjs().utc() : dayjs().tz(timezone);
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
const newValue = localMoment
|
||||
.set('year', newDate.getFullYear())
|
||||
.set('month', newDate.getMonth())
|
||||
.set('date', newDate.getDate())
|
||||
.set('hour', value.getHours())
|
||||
.set('minute', value.getMinutes())
|
||||
.set('second', value.getSeconds());
|
||||
const getTimeValueInSelectedTimezone = () => {
|
||||
const time = dateTime(valueInSelectedTimezone.format());
|
||||
time.set('hour', valueInSelectedTimezone.hour());
|
||||
time.set('minute', valueInSelectedTimezone.minute());
|
||||
time.set('second', valueInSelectedTimezone.second());
|
||||
return time;
|
||||
};
|
||||
|
||||
onChange(newValue);
|
||||
};
|
||||
const handleTimeChange = (newMoment: DateTime) => {
|
||||
const localMoment = dayjs().tz(timezone).utcOffset() === 0 ? dayjs().utc() : dayjs().tz(timezone);
|
||||
const newDate = newMoment.toDate();
|
||||
const newValue = localMoment
|
||||
.set('year', value.getFullYear())
|
||||
.set('month', value.getMonth())
|
||||
.set('date', value.getDate())
|
||||
.set('hour', newDate.getHours())
|
||||
.set('minute', newDate.getMinutes())
|
||||
.set('second', newDate.getSeconds());
|
||||
const getDateForDatePicker = () => {
|
||||
const date = new Date();
|
||||
date.setFullYear(valueInSelectedTimezone.year());
|
||||
date.setMonth(valueInSelectedTimezone.month());
|
||||
date.setDate(valueInSelectedTimezone.date());
|
||||
date.setHours(valueInSelectedTimezone.hour());
|
||||
date.setMinutes(valueInSelectedTimezone.minute());
|
||||
date.setSeconds(valueInSelectedTimezone.second());
|
||||
return date;
|
||||
};
|
||||
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<div style={{ display: 'flex', flexWrap: 'nowrap', gap: '8px' }}>
|
||||
<div
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={{ width: '58%' }}
|
||||
className={cx({ 'control--error': Boolean(error) })}
|
||||
>
|
||||
<DatePickerWithInput open minDate={minDate} disabled={disabled} value={value} onChange={handleDateChange} />
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<div style={{ display: 'flex', flexWrap: 'nowrap', gap: '8px' }}>
|
||||
<div
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={{ width: '58%' }}
|
||||
className={cx({ 'control--error': Boolean(error) })}
|
||||
>
|
||||
<DatePickerWithInput open disabled={disabled} value={getDateForDatePicker()} onChange={handleDateChange} />
|
||||
</div>
|
||||
<div
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={{ width: '42%' }}
|
||||
className={cx({ 'control--error': Boolean(error) })}
|
||||
data-testid="date-time-picker"
|
||||
>
|
||||
<TimeOfDayPicker disabled={disabled} value={getTimeValueInSelectedTimezone()} onChange={handleTimeChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={{ width: '42%' }}
|
||||
className={cx({ 'control--error': Boolean(error) })}
|
||||
>
|
||||
<TimeOfDayPicker disabled={disabled} value={dateTime(value)} onChange={handleTimeChange} />
|
||||
</div>
|
||||
</div>
|
||||
{error && <Text type="danger">{error}</Text>}
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
{error && <Text type="danger">{error}</Text>}
|
||||
</VerticalGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default DateTimePicker;
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
|||
|
||||
import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.types';
|
||||
import Text from 'components/Text/Text';
|
||||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import Rotation from 'containers/Rotation/Rotation';
|
||||
import RotationForm from 'containers/RotationForm/RotationForm';
|
||||
import TimelineMarks from 'containers/TimelineMarks/TimelineMarks';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { getColor, getLayersFromStore } from 'models/schedule/schedule.helpers';
|
||||
import { Layer, Schedule, ScheduleType, Shift, ShiftSwap, Event } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -29,8 +28,6 @@ import styles from './Rotations.module.css';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface RotationsProps extends WithStoreProps {
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
shiftIdToShowRotationForm?: Shift['id'] | 'new';
|
||||
scheduleId: Schedule['id'];
|
||||
onShowRotationForm: (shiftId: Shift['id'] | 'new') => void;
|
||||
|
|
@ -63,8 +60,6 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
render() {
|
||||
const {
|
||||
scheduleId,
|
||||
startMoment,
|
||||
currentTimezone,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
|
|
@ -78,13 +73,16 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
const { layerPriority, shiftStartToShowRotationForm, shiftEndToShowRotationForm } = this.state;
|
||||
|
||||
const base = 7 * 24 * 60; // in minutes
|
||||
const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes');
|
||||
const diff = store.timezoneStore.currentDateInSelectedTimezone.diff(
|
||||
store.timezoneStore.calendarStartDate,
|
||||
'minutes'
|
||||
);
|
||||
|
||||
const currentTimeX = diff / base;
|
||||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const layers = getLayersFromStore(store, scheduleId, startMoment);
|
||||
const layers = getLayersFromStore(store, scheduleId, store.timezoneStore.calendarStartDate);
|
||||
|
||||
const options = layers
|
||||
? layers.map((layer) => ({
|
||||
|
|
@ -136,7 +134,11 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
size="md"
|
||||
/>
|
||||
) : (
|
||||
<Button variant="primary" icon="plus" onClick={() => this.handleAddLayer(nextPriority, startMoment)}>
|
||||
<Button
|
||||
variant="primary"
|
||||
icon="plus"
|
||||
onClick={() => this.handleAddLayer(nextPriority, store.timezoneStore.calendarStartDate)}
|
||||
>
|
||||
Add rotation
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -155,7 +157,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
<TimelineMarks startMoment={startMoment} timezone={currentTimezone} />
|
||||
<TimelineMarks />
|
||||
{!currentTimeHidden && (
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
)}
|
||||
|
|
@ -177,8 +179,6 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
events={events}
|
||||
layerIndex={layerIndex}
|
||||
rotationIndex={rotationIndex}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
transparent={isPreview}
|
||||
tutorialParams={isPreview && store.scheduleStore.rotationFormLiveParams}
|
||||
filters={filters}
|
||||
|
|
@ -202,7 +202,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
<TimelineMarks startMoment={startMoment} timezone={currentTimezone} />
|
||||
<TimelineMarks />
|
||||
<div className={cx('rotations')}>
|
||||
<Rotation
|
||||
onClick={(shiftStart, shiftEnd) => {
|
||||
|
|
@ -211,8 +211,6 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
events={[]}
|
||||
layerIndex={0}
|
||||
rotationIndex={0}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -226,7 +224,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
this.handleAddLayer(nextPriority, startMoment);
|
||||
this.handleAddLayer(nextPriority, store.timezoneStore.calendarStartDate);
|
||||
}}
|
||||
>
|
||||
<Text type={disabled ? 'disabled' : 'primary'}>+ Add new layer with rotation</Text>
|
||||
|
|
@ -240,8 +238,6 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
shiftColor={findColor(shiftIdToShowRotationForm, layers)}
|
||||
scheduleId={scheduleId}
|
||||
layerPriority={layerPriority}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
shiftStart={shiftStartToShowRotationForm}
|
||||
shiftEnd={shiftEndToShowRotationForm}
|
||||
onHide={() => {
|
||||
|
|
@ -299,7 +295,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
};
|
||||
|
||||
handleAddRotation = (option: SelectableValue) => {
|
||||
const { startMoment, disabled } = this.props;
|
||||
const { disabled, store } = this.props;
|
||||
|
||||
if (disabled) {
|
||||
return;
|
||||
|
|
@ -308,7 +304,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
|
|||
this.setState(
|
||||
{
|
||||
layerPriority: option.value,
|
||||
shiftStartToShowRotationForm: startMoment,
|
||||
shiftStartToShowRotationForm: store.timezoneStore.calendarStartDate,
|
||||
},
|
||||
() => {
|
||||
this.onShowRotationForm('new');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
|
||||
import { HorizontalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
|
@ -8,8 +8,8 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
|||
|
||||
import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.types';
|
||||
import Text from 'components/Text/Text';
|
||||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import Rotation from 'containers/Rotation/Rotation';
|
||||
import TimelineMarks from 'containers/TimelineMarks/TimelineMarks';
|
||||
import {
|
||||
flattenShiftEvents,
|
||||
getLayersFromStore,
|
||||
|
|
@ -17,7 +17,6 @@ import {
|
|||
getShiftsFromStore,
|
||||
} from 'models/schedule/schedule.helpers';
|
||||
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';
|
||||
|
||||
|
|
@ -29,8 +28,6 @@ import styles from './Rotations.module.css';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface ScheduleFinalProps extends WithStoreProps {
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
scheduleId: Schedule['id'];
|
||||
simplified?: boolean;
|
||||
onShowOverrideForm: (shiftId: 'new', shiftStart: dayjs.Dayjs, shiftEnd: dayjs.Dayjs) => void;
|
||||
|
|
@ -40,82 +37,80 @@ interface ScheduleFinalProps extends WithStoreProps {
|
|||
onSlotClick?: (event: Event) => void;
|
||||
}
|
||||
|
||||
@observer
|
||||
class ScheduleFinal extends Component<ScheduleFinalProps> {
|
||||
render() {
|
||||
const { startMoment, currentTimezone, store, simplified, scheduleId, filters, onShowShiftSwapForm, onSlotClick } =
|
||||
this.props;
|
||||
|
||||
const ScheduleFinal: FC<ScheduleFinalProps> = observer(
|
||||
({ store, simplified, scheduleId, filters, onShowShiftSwapForm, onShowOverrideForm, onSlotClick }) => {
|
||||
const {
|
||||
timezoneStore: { currentDateInSelectedTimezone, calendarStartDate, selectedTimezoneOffset },
|
||||
scheduleStore: { refreshEvents },
|
||||
} = store;
|
||||
const base = 7 * 24 * 60; // in minutes
|
||||
const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes');
|
||||
const diff = currentDateInSelectedTimezone.diff(calendarStartDate, 'minutes');
|
||||
|
||||
const currentTimeX = diff / base;
|
||||
|
||||
const shifts = flattenShiftEvents(getShiftsFromStore(store, scheduleId, startMoment));
|
||||
const shifts = flattenShiftEvents(getShiftsFromStore(store, scheduleId, calendarStartDate));
|
||||
|
||||
const layers = getLayersFromStore(store, scheduleId, startMoment);
|
||||
const layers = getLayersFromStore(store, scheduleId, calendarStartDate);
|
||||
|
||||
const overrides = getOverridesFromStore(store, scheduleId, startMoment);
|
||||
const overrides = getOverridesFromStore(store, scheduleId, calendarStartDate);
|
||||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const getColor = (event: Event) => findColor(event.shift?.pk, layers, overrides);
|
||||
|
||||
const handleShowOverrideForm = (shiftStart: dayjs.Dayjs, shiftEnd: dayjs.Dayjs) => {
|
||||
onShowOverrideForm('new', shiftStart, shiftEnd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshEvents(scheduleId);
|
||||
}, [selectedTimezoneOffset]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
{!simplified && (
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={cx('title')}>
|
||||
<Text.Title level={4} type="primary">
|
||||
Final schedule
|
||||
</Text.Title>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks startMoment={startMoment} timezone={currentTimezone} />
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
{shifts && shifts.length ? (
|
||||
shifts.map(({ events }, index) => {
|
||||
return (
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
key={index}
|
||||
events={events}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
handleAddOverride={this.handleShowOverrideForm}
|
||||
handleAddShiftSwap={onShowShiftSwapForm}
|
||||
onShiftSwapClick={onShowShiftSwapForm}
|
||||
simplified={simplified}
|
||||
filters={filters}
|
||||
getColor={getColor}
|
||||
onSlotClick={onSlotClick}
|
||||
/>
|
||||
</CSSTransition>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation events={[]} startMoment={startMoment} currentTimezone={currentTimezone} />
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
<div className={cx('root')}>
|
||||
{!simplified && (
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={cx('title')}>
|
||||
<Text.Title level={4} type="primary">
|
||||
Final schedule
|
||||
</Text.Title>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks />
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
{shifts && shifts.length ? (
|
||||
shifts.map(({ events }, index) => {
|
||||
return (
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
key={index}
|
||||
events={events}
|
||||
handleAddOverride={handleShowOverrideForm}
|
||||
handleAddShiftSwap={onShowShiftSwapForm}
|
||||
onShiftSwapClick={onShowShiftSwapForm}
|
||||
simplified={simplified}
|
||||
filters={filters}
|
||||
getColor={getColor}
|
||||
onSlotClick={onSlotClick}
|
||||
/>
|
||||
</CSSTransition>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation events={[]} />
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleShowOverrideForm = (shiftStart: dayjs.Dayjs, shiftEnd: dayjs.Dayjs) => {
|
||||
const { onShowOverrideForm } = this.props;
|
||||
|
||||
onShowOverrideForm('new', shiftStart, shiftEnd);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export default withMobXProviderContext(ScheduleFinal);
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
|||
|
||||
import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.types';
|
||||
import Text from 'components/Text/Text';
|
||||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import Rotation from 'containers/Rotation/Rotation';
|
||||
import ScheduleOverrideForm from 'containers/RotationForm/ScheduleOverrideForm';
|
||||
import TimelineMarks from 'containers/TimelineMarks/TimelineMarks';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import {
|
||||
getLayersFromStore,
|
||||
|
|
@ -20,8 +20,7 @@ import {
|
|||
SHIFT_SWAP_COLOR,
|
||||
} from 'models/schedule/schedule.helpers';
|
||||
import { Schedule, Shift, ShiftEvents, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { getStartOfDay, getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { getUTCString } from 'pages/schedule/Schedule.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
|
@ -34,10 +33,8 @@ import styles from './Rotations.module.css';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface ScheduleOverridesProps extends WithStoreProps {
|
||||
startMoment: dayjs.Dayjs;
|
||||
shiftStartToShowOverrideForm: dayjs.Dayjs;
|
||||
shiftEndToShowOverrideForm: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
scheduleId: Schedule['id'];
|
||||
shiftIdToShowRotationForm?: Shift['id'] | 'new';
|
||||
onShowRotationForm: (shiftId: Shift['id'] | 'new') => void;
|
||||
|
|
@ -65,8 +62,6 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
render() {
|
||||
const {
|
||||
scheduleId,
|
||||
startMoment,
|
||||
currentTimezone,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
|
|
@ -84,14 +79,17 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
} = this.props;
|
||||
const { shiftStartToShowOverrideForm, shiftEndToShowOverrideForm } = this.state;
|
||||
|
||||
const shifts = getOverridesFromStore(store, scheduleId, startMoment) as ShiftEvents[];
|
||||
const shifts = getOverridesFromStore(store, scheduleId, store.timezoneStore.calendarStartDate) as ShiftEvents[];
|
||||
|
||||
const layers = getLayersFromStore(store, scheduleId, startMoment);
|
||||
const layers = getLayersFromStore(store, scheduleId, store.timezoneStore.calendarStartDate);
|
||||
|
||||
const shiftSwaps = getShiftSwapsFromStore(store, scheduleId, startMoment);
|
||||
const shiftSwaps = getShiftSwapsFromStore(store, scheduleId, store.timezoneStore.calendarStartDate);
|
||||
|
||||
const base = 7 * 24 * 60; // in minutes
|
||||
const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes');
|
||||
const diff = store.timezoneStore.currentDateInSelectedTimezone.diff(
|
||||
store.timezoneStore.calendarStartDate,
|
||||
'minutes'
|
||||
);
|
||||
|
||||
const currentTimeX = diff / base;
|
||||
|
||||
|
|
@ -119,7 +117,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
const closestEvent = findClosestUserEvent(dayjs(), currentUserPk, layers);
|
||||
const swapStart = closestEvent
|
||||
? dayjs(closestEvent.start)
|
||||
: dayjs().tz(currentTimezone).startOf('day').add(1, 'day');
|
||||
: store.timezoneStore.currentDateInSelectedTimezone.startOf('day').add(1, 'day');
|
||||
|
||||
const swapEnd = closestEvent ? dayjs(closestEvent.end) : swapStart.add(1, 'day');
|
||||
|
||||
|
|
@ -151,7 +149,7 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks startMoment={startMoment} timezone={currentTimezone} />
|
||||
<TimelineMarks />
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
{shiftSwaps && shiftSwaps.length
|
||||
? shiftSwaps.map(({ isPreview, events }, index) => (
|
||||
|
|
@ -159,8 +157,6 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
<Rotation
|
||||
events={events}
|
||||
color={SHIFT_SWAP_COLOR}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
onSlotClick={(event) => {
|
||||
if (event.is_gap) {
|
||||
return;
|
||||
|
|
@ -181,8 +177,6 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
<Rotation
|
||||
events={events}
|
||||
color={getOverrideColor(index)}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
onClick={(shiftStart, shiftEnd) => {
|
||||
this.onRotationClick(shiftId, shiftStart, shiftEnd);
|
||||
}}
|
||||
|
|
@ -196,8 +190,6 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
<Rotation
|
||||
key={0}
|
||||
events={[]}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
onClick={(shiftStart, shiftEnd) => {
|
||||
this.onRotationClick('new', shiftStart, shiftEnd);
|
||||
}}
|
||||
|
|
@ -212,8 +204,6 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
shiftId={shiftIdToShowRotationForm}
|
||||
shiftColor={findColor(shiftIdToShowRotationForm, undefined, shifts)}
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
shiftStart={propsShiftStartToShowOverrideForm || shiftStartToShowOverrideForm}
|
||||
shiftEnd={propsShiftEndToShowOverrideForm || shiftEndToShowOverrideForm}
|
||||
onHide={() => {
|
||||
|
|
@ -262,11 +252,12 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
|
|||
}
|
||||
|
||||
// use start of current day as default start time for override
|
||||
const startMoment = getStartOfDay(store.currentTimezone);
|
||||
|
||||
this.setState({ shiftStartToShowOverrideForm: startMoment }, () => {
|
||||
this.onShowRotationForm('new');
|
||||
});
|
||||
this.setState(
|
||||
{ shiftStartToShowOverrideForm: store.timezoneStore.currentDateInSelectedTimezone.startOf('day') },
|
||||
() => {
|
||||
this.onShowRotationForm('new');
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
handleHide = () => {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
|
||||
import { Badge, Button, HorizontalGroup, Icon } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import Text from 'components/Text/Text';
|
||||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import Rotation from 'containers/Rotation/Rotation';
|
||||
import TimelineMarks from 'containers/TimelineMarks/TimelineMarks';
|
||||
import { ActionKey } from 'models/loader/action-keys';
|
||||
import { getColorForSchedule, getPersonalShiftsFromStore } from 'models/schedule/schedule.helpers';
|
||||
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 { getStartOfWeekBasedOnCurrentDate } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config';
|
||||
|
|
@ -26,175 +24,127 @@ import styles from './Rotations.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface SchedulePersonalProps extends WithStoreProps, RouteComponentProps {
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
interface SchedulePersonalProps extends RouteComponentProps {
|
||||
userPk: User['pk'];
|
||||
onSlotClick?: (event: Event) => void;
|
||||
}
|
||||
|
||||
interface SchedulePersonalState {
|
||||
startMoment?: dayjs.Dayjs;
|
||||
}
|
||||
const SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotClick, history }) => {
|
||||
const store = useStore();
|
||||
const { timezoneStore, scheduleStore, userStore, loaderStore } = store;
|
||||
|
||||
@observer
|
||||
class SchedulePersonal extends Component<SchedulePersonalProps, SchedulePersonalState> {
|
||||
state: SchedulePersonalState = {};
|
||||
useEffect(() => {
|
||||
updatePersonalEvents();
|
||||
}, [timezoneStore.selectedTimezoneOffset]);
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
startMoment: props.startMoment,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { store } = this.props;
|
||||
const { startMoment } = this.state;
|
||||
|
||||
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) });
|
||||
const updatePersonalEvents = () => {
|
||||
scheduleStore.updatePersonalEvents(userStore.currentUserPk, timezoneStore.calendarStartDate, 9, true);
|
||||
};
|
||||
|
||||
handleLeftClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
|
||||
this.setState({ startMoment: startMoment.add(-7, 'day') });
|
||||
const handleTodayClick = () => {
|
||||
timezoneStore.setCalendarStartDate(getStartOfWeekBasedOnCurrentDate(timezoneStore.currentDateInSelectedTimezone));
|
||||
};
|
||||
|
||||
handleRightClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
|
||||
this.setState({ startMoment: startMoment.add(7, 'day') });
|
||||
const handleLeftClick = () => {
|
||||
timezoneStore.setCalendarStartDate(timezoneStore.calendarStartDate.subtract(7, 'day'));
|
||||
scheduleStore.updatePersonalEvents(userStore.currentUserPk, timezoneStore.calendarStartDate);
|
||||
};
|
||||
|
||||
render() {
|
||||
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');
|
||||
|
||||
const currentTimeX = diff / base;
|
||||
|
||||
const shifts = getPersonalShiftsFromStore(store, userPk, startMoment);
|
||||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const getColor = (event: Event) => getColorForSchedule(event.schedule?.id);
|
||||
|
||||
const isOncall = store.scheduleStore.onCallNow[userPk];
|
||||
|
||||
const storeUser = store.userStore.items[userPk];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('header')}>
|
||||
<div className={cx('title')}>
|
||||
<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>
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks startMoment={startMoment} timezone={currentTimezone} />
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
{shifts && shifts.length ? (
|
||||
shifts.map(({ events }, index) => {
|
||||
return (
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
simplified
|
||||
key={index}
|
||||
events={events}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
getColor={getColor}
|
||||
onSlotClick={onSlotClick}
|
||||
handleOpenSchedule={this.openSchedule}
|
||||
showScheduleNameAsSlotTitle
|
||||
/>
|
||||
</CSSTransition>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
events={[]}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
emptyText="There are no schedules relevant to user"
|
||||
/>
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
openSchedule = (event: Event) => {
|
||||
const { history } = this.props;
|
||||
const handleRightClick = () => {
|
||||
timezoneStore.setCalendarStartDate(timezoneStore.calendarStartDate.add(7, 'day'));
|
||||
scheduleStore.updatePersonalEvents(userStore.currentUserPk, timezoneStore.calendarStartDate);
|
||||
};
|
||||
|
||||
const openSchedule = (event: Event) => {
|
||||
history.push(`${PLUGIN_ROOT}/schedules/${event.schedule?.id}`);
|
||||
};
|
||||
}
|
||||
|
||||
export default withRouter(withMobXProviderContext(SchedulePersonal));
|
||||
const base = 7 * 24 * 60; // in minutes
|
||||
const diff = timezoneStore.currentDateInSelectedTimezone.diff(timezoneStore.calendarStartDate, 'minutes');
|
||||
|
||||
const currentTimeX = diff / base;
|
||||
|
||||
const shifts = getPersonalShiftsFromStore(store, userPk, timezoneStore.calendarStartDate);
|
||||
|
||||
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
|
||||
|
||||
const getColor = (event: Event) => getColorForSchedule(event.schedule?.id);
|
||||
|
||||
const isOncall = scheduleStore.onCallNow[userPk];
|
||||
|
||||
const storeUser = userStore.items[userPk];
|
||||
|
||||
const emptyRotationsText = loaderStore.isLoading(ActionKey.UPDATE_PERSONAL_EVENTS)
|
||||
? 'Loading ...'
|
||||
: 'There are no schedules relevant to user';
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('header')}>
|
||||
<div className={cx('title')}>
|
||||
<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">
|
||||
{timezoneStore.calendarStartDate.format('DD MMM')} -{' '}
|
||||
{timezoneStore.calendarStartDate.add(6, 'day').format('DD MMM')}
|
||||
</Text>
|
||||
<Button variant="secondary" size="sm" onClick={handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Button variant="secondary" size="sm" onClick={handleLeftClick}>
|
||||
<Icon name="angle-left" />
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={handleRightClick}>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks />
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
{shifts?.length ? (
|
||||
shifts.map(({ events }, index) => {
|
||||
return (
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation
|
||||
simplified
|
||||
key={index}
|
||||
events={events}
|
||||
getColor={getColor}
|
||||
onSlotClick={onSlotClick}
|
||||
handleOpenSchedule={openSchedule}
|
||||
showScheduleNameAsSlotTitle
|
||||
/>
|
||||
</CSSTransition>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<Rotation events={[]} emptyText={emptyRotationsText} />
|
||||
</CSSTransition>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default withRouter(SchedulePersonal);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import Text from 'components/Text/Text';
|
|||
import WorkingHours from 'components/WorkingHours/WorkingHours';
|
||||
import { getShiftName, SHIFT_SWAP_COLOR } from 'models/schedule/schedule.helpers';
|
||||
import { Event, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { getOffsetOfCurrentUser, getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
|
|
@ -22,8 +21,6 @@ import styles from './ScheduleSlot.module.css';
|
|||
|
||||
interface ScheduleSlotProps {
|
||||
event: Event;
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentTimezone: Timezone;
|
||||
handleAddOverride: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
handleAddShiftSwap: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
handleOpenSchedule: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
|
|
@ -39,7 +36,6 @@ const cx = cn.bind(styles);
|
|||
const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
||||
const {
|
||||
event,
|
||||
currentTimezone,
|
||||
color,
|
||||
handleAddOverride,
|
||||
handleAddShiftSwap,
|
||||
|
|
@ -63,12 +59,12 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
|
||||
const renderEvent = (event): React.ReactElement | React.ReactElement[] => {
|
||||
if (event.shiftSwapId) {
|
||||
return <ShiftSwapEvent currentMoment={currentMoment} event={event} currentTimezone={currentTimezone} />;
|
||||
return <ShiftSwapEvent currentMoment={currentMoment} event={event} />;
|
||||
}
|
||||
|
||||
if (event.is_gap) {
|
||||
return (
|
||||
<Tooltip content={<ScheduleGapDetails event={event} currentTimezone={currentTimezone} />}>
|
||||
<Tooltip content={<ScheduleGapDetails event={event} />}>
|
||||
<div className={cx('root', 'root__type_gap')} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
@ -95,7 +91,6 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
filters={filters}
|
||||
start={start}
|
||||
duration={duration}
|
||||
currentTimezone={currentTimezone}
|
||||
color={color}
|
||||
currentMoment={currentMoment}
|
||||
showScheduleNameAsSlotTitle={showScheduleNameAsSlotTitle}
|
||||
|
|
@ -114,12 +109,11 @@ export default ScheduleSlot;
|
|||
|
||||
interface ShiftSwapEventProps {
|
||||
event: Event;
|
||||
currentTimezone: Timezone;
|
||||
currentMoment: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
const ShiftSwapEvent = (props: ShiftSwapEventProps) => {
|
||||
const { event, currentTimezone, currentMoment } = props;
|
||||
const { event, currentMoment } = props;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
|
|
@ -144,7 +138,7 @@ const ShiftSwapEvent = (props: ShiftSwapEventProps) => {
|
|||
const benefactorStoreUser = store.userStore.items[shiftSwap?.benefactor?.pk];
|
||||
|
||||
const scheduleSlotContent = (
|
||||
<div className={cx('root', { 'root__type_shift-swap': true })}>
|
||||
<div className={cx('root', { 'root__type_shift-swap': true })} data-testid="schedule-slot">
|
||||
{shiftSwap && (
|
||||
<HorizontalGroup spacing="xs">
|
||||
{beneficiary && <Avatar size="xs" src={beneficiary.avatar_full} />}
|
||||
|
|
@ -176,7 +170,6 @@ const ShiftSwapEvent = (props: ShiftSwapEventProps) => {
|
|||
beneficiaryName={beneficiary?.display_name}
|
||||
user={benefactorStoreUser || beneficiaryStoreUser}
|
||||
benefactorName={benefactor?.display_name}
|
||||
currentTimezone={currentTimezone}
|
||||
event={event}
|
||||
color={SHIFT_SWAP_COLOR}
|
||||
currentMoment={currentMoment}
|
||||
|
|
@ -190,7 +183,6 @@ const ShiftSwapEvent = (props: ShiftSwapEventProps) => {
|
|||
|
||||
interface RegularEventProps {
|
||||
event: Event;
|
||||
currentTimezone: Timezone;
|
||||
handleAddOverride: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
handleAddShiftSwap: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
handleOpenSchedule: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
|
|
@ -209,7 +201,6 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
onShiftSwapClick,
|
||||
filters,
|
||||
color,
|
||||
currentTimezone,
|
||||
start,
|
||||
duration,
|
||||
handleAddOverride,
|
||||
|
|
@ -261,6 +252,7 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
backgroundColor,
|
||||
}}
|
||||
onClick={swap_request ? getShiftSwapClickHandler(swap_request.pk) : undefined}
|
||||
data-testid="schedule-slot"
|
||||
>
|
||||
{storeUser && (!swap_request || swap_request.user) && (
|
||||
<WorkingHours
|
||||
|
|
@ -294,7 +286,6 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
}
|
||||
benefactorName={isShiftSwap ? (swap_request.user ? display_name : undefined) : undefined}
|
||||
user={storeUser}
|
||||
currentTimezone={currentTimezone}
|
||||
event={event}
|
||||
handleAddOverride={
|
||||
!handleAddOverride || event.is_override || isShiftSwap || currentMoment.isAfter(dayjs(event.end))
|
||||
|
|
@ -323,7 +314,6 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
interface ScheduleSlotDetailsProps {
|
||||
user: User;
|
||||
isOncall?: boolean;
|
||||
currentTimezone: Timezone;
|
||||
event: Event;
|
||||
handleAddOverride?: (event: React.SyntheticEvent) => void;
|
||||
handleAddShiftSwap?: (event: React.SyntheticEvent) => void;
|
||||
|
|
@ -336,10 +326,9 @@ interface ScheduleSlotDetailsProps {
|
|||
title: string;
|
||||
}
|
||||
|
||||
const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
||||
const ScheduleSlotDetails = observer((props: ScheduleSlotDetailsProps) => {
|
||||
const {
|
||||
user,
|
||||
currentTimezone,
|
||||
event,
|
||||
handleAddOverride,
|
||||
handleAddShiftSwap,
|
||||
|
|
@ -352,7 +341,10 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
title,
|
||||
} = props;
|
||||
|
||||
const { scheduleStore } = useStore();
|
||||
const {
|
||||
scheduleStore,
|
||||
timezoneStore: { currentDateInSelectedTimezone, getDateInSelectedTimezone },
|
||||
} = useStore();
|
||||
|
||||
const shiftId = event.shift?.pk;
|
||||
const shift = scheduleStore.shifts[shiftId];
|
||||
|
|
@ -418,7 +410,7 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
<Icon className={cx('icon')} name="clock-nine" />
|
||||
</div>
|
||||
<Text type="primary" className={cx('second-column')}>
|
||||
User local time
|
||||
User's local time
|
||||
<br />
|
||||
{currentMoment.tz(user?.timezone).format('DD MMM, HH:mm')}
|
||||
<br />({getTzOffsetString(currentMoment.tz(user?.timezone))})
|
||||
|
|
@ -426,8 +418,8 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
<Text type="secondary">
|
||||
Current timezone
|
||||
<br />
|
||||
{currentMoment.tz(currentTimezone).format('DD MMM, HH:mm')}
|
||||
<br />({getTzOffsetString(currentMoment.tz(currentTimezone))})
|
||||
{currentDateInSelectedTimezone.format('DD MMM, HH:mm')}
|
||||
<br />({getTzOffsetString(currentDateInSelectedTimezone)})
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
|
|
@ -437,15 +429,15 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
<Text type="primary" className={cx('second-column')}>
|
||||
This shift
|
||||
<br />
|
||||
{dayjs(event.start).tz(user?.timezone).format('DD MMM, HH:mm')}
|
||||
{dayjs(event.start).utcOffset(getOffsetOfCurrentUser()).format('DD MMM, HH:mm')}
|
||||
<br />
|
||||
{dayjs(event.end).tz(user?.timezone).format('DD MMM, HH:mm')}
|
||||
{dayjs(event.end).utcOffset(getOffsetOfCurrentUser()).format('DD MMM, HH:mm')}
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
<br />
|
||||
{dayjs(event.start).tz(currentTimezone).format('DD MMM, HH:mm')}
|
||||
{getDateInSelectedTimezone(dayjs(event.start)).format('DD MMM, HH:mm')}
|
||||
<br />
|
||||
{dayjs(event.end).tz(currentTimezone).format('DD MMM, HH:mm')}
|
||||
{getDateInSelectedTimezone(dayjs(event.end)).format('DD MMM, HH:mm')}
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
|
|
@ -468,27 +460,29 @@ const ScheduleSlotDetails = (props: ScheduleSlotDetailsProps) => {
|
|||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
interface ScheduleGapDetailsProps {
|
||||
currentTimezone: Timezone;
|
||||
event: Event;
|
||||
}
|
||||
|
||||
const ScheduleGapDetails = (props: ScheduleGapDetailsProps) => {
|
||||
const { currentTimezone, event } = props;
|
||||
const ScheduleGapDetails = observer((props: ScheduleGapDetailsProps) => {
|
||||
const {
|
||||
timezoneStore: { selectedTimezoneLabel, getDateInSelectedTimezone },
|
||||
} = useStore();
|
||||
const { event } = props;
|
||||
|
||||
return (
|
||||
<div className={cx('details')}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup spacing="sm">
|
||||
<VerticalGroup spacing="none">
|
||||
<Text type="primary">{currentTimezone}</Text>
|
||||
<Text type="primary">{dayjs(event.start).tz(currentTimezone).format('DD MMM, HH:mm')}</Text>
|
||||
<Text type="primary">{dayjs(event.end).tz(currentTimezone).format('DD MMM, HH:mm')}</Text>
|
||||
<Text type="primary">{selectedTimezoneLabel}</Text>
|
||||
<Text type="primary">{getDateInSelectedTimezone(dayjs(event.start)).format('DD MMM, HH:mm')}</Text>
|
||||
<Text type="primary">{getDateInSelectedTimezone(dayjs(event.end)).format('DD MMM, HH:mm')}</Text>
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,25 +2,24 @@ import React, { FC, useMemo } from 'react';
|
|||
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { getNow } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './TimelineMarks.module.scss';
|
||||
|
||||
interface TimelineMarksProps {
|
||||
startMoment: dayjs.Dayjs;
|
||||
timezone: Timezone;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const TimelineMarks: FC<TimelineMarksProps> = (props) => {
|
||||
const { startMoment, timezone, debug } = props;
|
||||
|
||||
const currentMoment = useMemo(() => getNow(timezone), []);
|
||||
const TimelineMarks: FC<TimelineMarksProps> = observer((props) => {
|
||||
const {
|
||||
timezoneStore: { currentDateInSelectedTimezone, calendarStartDate },
|
||||
} = useStore();
|
||||
const { debug } = props;
|
||||
|
||||
const momentsToRender = useMemo(() => {
|
||||
const hoursToSplit = 12;
|
||||
|
|
@ -29,7 +28,7 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
|
|||
const jLimit = 24 / hoursToSplit;
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = dayjs(startMoment).add(i, 'days');
|
||||
const d = dayjs(calendarStartDate).add(i, 'days');
|
||||
const obj = { moment: d, moments: [] };
|
||||
for (let j = 0; j < jLimit; j++) {
|
||||
const m = dayjs(d).add(j * hoursToSplit, 'hour');
|
||||
|
|
@ -38,7 +37,7 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
|
|||
momentsToRender.push(obj);
|
||||
}
|
||||
return momentsToRender;
|
||||
}, [startMoment]);
|
||||
}, [calendarStartDate]);
|
||||
|
||||
const cuts = useMemo(() => {
|
||||
const cuts = [];
|
||||
|
|
@ -67,7 +66,7 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
|
|||
)}
|
||||
|
||||
{momentsToRender.map((m, i) => {
|
||||
const isCurrentDay = currentMoment.isSame(m.moment, 'day');
|
||||
const isCurrentDay = currentDateInSelectedTimezone.isSame(m.moment, 'day');
|
||||
|
||||
// const isWeekend = m.moment.day() == 0 || m.moment.day() === 6;
|
||||
|
||||
|
|
@ -96,6 +95,6 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
|
|||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default TimelineMarks;
|
||||
|
|
@ -4,100 +4,67 @@ import { SelectableValue } from '@grafana/data';
|
|||
import { Select } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { sortBy } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { getTzOffsetString, allTimezones } from 'models/timezone/timezone.helpers';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { allTimezones, getGMTTimezoneLabelBasedOnOffset, getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './UserTimezoneSelect.module.css';
|
||||
|
||||
interface UserTimezoneSelectProps {
|
||||
users: User[];
|
||||
value: Timezone;
|
||||
onChange: (value: Timezone) => void;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface TimezoneOption {
|
||||
value: number;
|
||||
utcOffset: number;
|
||||
timezone: Timezone;
|
||||
value: number; // utcOffset
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const UserTimezoneSelect: FC<UserTimezoneSelectProps> = (props) => {
|
||||
const { users, value: propValue, onChange } = props;
|
||||
interface UserTimezoneSelectProps {
|
||||
scheduleId?: string;
|
||||
}
|
||||
|
||||
const UserTimezoneSelect: FC<UserTimezoneSelectProps> = observer(({ scheduleId }) => {
|
||||
const store = useStore();
|
||||
const users = store.userStore.getSearchResult().results || [];
|
||||
|
||||
const [extraOptions, setExtraOptions] = useState<TimezoneOption[]>([
|
||||
{
|
||||
value: 0,
|
||||
utcOffset: 0,
|
||||
timezone: 'UTC' as Timezone,
|
||||
label: 'GMT',
|
||||
description: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return users
|
||||
.reduce(
|
||||
return sortBy(
|
||||
users.reduce(
|
||||
(memo, user) => {
|
||||
const moment = dayjs().tz(user.timezone);
|
||||
const utcOffset = moment.utcOffset();
|
||||
|
||||
let item = memo.find((item) => item.utcOffset === utcOffset);
|
||||
let item = memo.find((item) => item.value === utcOffset);
|
||||
|
||||
if (!item) {
|
||||
item = {
|
||||
value: utcOffset,
|
||||
utcOffset,
|
||||
timezone: user.timezone,
|
||||
label: getTzOffsetString(moment),
|
||||
label: getGMTTimezoneLabelBasedOnOffset(utcOffset),
|
||||
description: user.username,
|
||||
};
|
||||
|
||||
memo.push(item);
|
||||
} else {
|
||||
item.description += item.description ? ', ' + user.username : user.username;
|
||||
// item.imgUrl = undefined;
|
||||
}
|
||||
|
||||
return memo;
|
||||
},
|
||||
[...extraOptions.map((option) => ({ ...option }))]
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (b.utcOffset === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.utcOffset > b.utcOffset) {
|
||||
return 1;
|
||||
}
|
||||
if (a.utcOffset < b.utcOffset) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
),
|
||||
({ value }) => value
|
||||
);
|
||||
}, [users, extraOptions]);
|
||||
|
||||
const value = useMemo(() => {
|
||||
const utcOffset = dayjs().tz(propValue).utcOffset();
|
||||
const option = options.find((option) => option.utcOffset === utcOffset);
|
||||
|
||||
return option?.value;
|
||||
}, [propValue, options]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ value }) => {
|
||||
const option = options.find((option) => option.utcOffset === value);
|
||||
onChange(option?.timezone);
|
||||
},
|
||||
[options]
|
||||
);
|
||||
|
||||
const filterOption = useCallback((item: SelectableValue<number>, searchQuery: string) => {
|
||||
const { data } = item;
|
||||
|
||||
|
|
@ -105,7 +72,7 @@ const UserTimezoneSelect: FC<UserTimezoneSelectProps> = (props) => {
|
|||
if (data.__isNew_) {
|
||||
return true;
|
||||
}
|
||||
return data[key] && data[key].toLowerCase().includes(searchQuery.toLowerCase());
|
||||
return data[key]?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
|
@ -115,9 +82,10 @@ const UserTimezoneSelect: FC<UserTimezoneSelectProps> = (props) => {
|
|||
if (matched) {
|
||||
const now = dayjs().tz(matched);
|
||||
const utcOffset = now.utcOffset();
|
||||
onChange(matched);
|
||||
store.timezoneStore.setSelectedTimezoneOffset(utcOffset);
|
||||
store.scheduleStore.refreshEvents(scheduleId);
|
||||
|
||||
if (options.some((option) => option.utcOffset === utcOffset)) {
|
||||
if (options.some((option) => option.value === utcOffset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -125,26 +93,25 @@ const UserTimezoneSelect: FC<UserTimezoneSelectProps> = (props) => {
|
|||
...extraOptions,
|
||||
{
|
||||
value: utcOffset,
|
||||
utcOffset,
|
||||
timezone: matched,
|
||||
label: getTzOffsetString(now),
|
||||
description: '',
|
||||
},
|
||||
]);
|
||||
|
||||
onChange(matched);
|
||||
store.timezoneStore.setSelectedTimezoneOffset(utcOffset);
|
||||
store.scheduleStore.refreshEvents(scheduleId);
|
||||
}
|
||||
},
|
||||
[options]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('root')} data-testid="timezone-select">
|
||||
<Select
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
value={options.find(({ value }) => value === store.timezoneStore.selectedTimezoneOffset)}
|
||||
onChange={(option) => store.timezoneStore.setSelectedTimezoneOffset(option.value)}
|
||||
width={30}
|
||||
placeholder={propValue}
|
||||
options={options}
|
||||
filterOption={filterOption}
|
||||
allowCustomValue
|
||||
|
|
@ -161,6 +128,6 @@ const UserTimezoneSelect: FC<UserTimezoneSelectProps> = (props) => {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default UserTimezoneSelect;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
.root {
|
||||
width: 210px;
|
||||
width: 260px;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
|
|
@ -3,12 +3,17 @@ import React, { FC } from 'react';
|
|||
import { HorizontalGroup, VerticalGroup, Icon, Badge } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import ScheduleBorderedAvatar from 'components/ScheduleBorderedAvatar/ScheduleBorderedAvatar';
|
||||
import Text from 'components/Text/Text';
|
||||
import { isInWorkingHours } from 'components/WorkingHours/WorkingHours.helpers';
|
||||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import {
|
||||
getCurrentDateInTimezone,
|
||||
getCurrentlyLoggedInUserDate,
|
||||
getTzOffsetString,
|
||||
} from 'models/timezone/timezone.helpers';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { getColorSchemeMappingForUsers } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -21,19 +26,21 @@ interface ScheduleUserDetailsProps {
|
|||
user: User;
|
||||
isOncall: boolean;
|
||||
scheduleId: string;
|
||||
startMoment: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (props) => {
|
||||
const { user, currentMoment, isOncall, scheduleId, startMoment } = props;
|
||||
const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = observer((props) => {
|
||||
const {
|
||||
timezoneStore: { calendarStartDate },
|
||||
} = useStore();
|
||||
const { user, currentMoment, isOncall, scheduleId } = props;
|
||||
const userMoment = currentMoment.tz(user.timezone);
|
||||
const userOffsetHoursStr = getTzOffsetString(userMoment);
|
||||
const isInWH = isInWorkingHours(currentMoment, user.working_hours, user.timezone);
|
||||
|
||||
const store = useStore();
|
||||
const colorSchemeMapping = getColorSchemeMappingForUsers(store, scheduleId, startMoment);
|
||||
const colorSchemeMapping = getColorSchemeMappingForUsers(store, scheduleId, calendarStartDate);
|
||||
const colorSchemeList = Array.from(colorSchemeMapping[user.pk] || []);
|
||||
|
||||
const { organizationStore } = store;
|
||||
|
|
@ -49,7 +56,7 @@ const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (props) => {
|
|||
height={35}
|
||||
renderAvatar={() => <Avatar src={user.avatar} size="large" />}
|
||||
renderIcon={() => null}
|
||||
></ScheduleBorderedAvatar>
|
||||
/>
|
||||
|
||||
<VerticalGroup spacing="xs" width="100%">
|
||||
<div className={cx('username')}>
|
||||
|
|
@ -70,18 +77,18 @@ const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (props) => {
|
|||
</Text>
|
||||
</div>
|
||||
<div className={cx('timezone-wrapper')}>
|
||||
<div className={cx('timezone-info')}>
|
||||
<div className={cx('timezone-info')} data-testid="schedule-user-details_your-current-time">
|
||||
<VerticalGroup spacing="none">
|
||||
<Text type="secondary">Local time</Text>
|
||||
<Text type="secondary">{currentMoment.tz().format('DD MMM, HH:mm')}</Text>
|
||||
<Text type="secondary">({getTzOffsetString(currentMoment)})</Text>
|
||||
<Text type="secondary">Your current time</Text>
|
||||
<Text type="secondary">{getCurrentlyLoggedInUserDate().format('DD MMM, HH:mm')}</Text>
|
||||
<Text type="secondary">({getTzOffsetString(getCurrentlyLoggedInUserDate())})</Text>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
|
||||
<div className={cx('timezone-info')}>
|
||||
<div className={cx('timezone-info')} data-testid="schedule-user-details_user-local-time">
|
||||
<VerticalGroup className={cx('timezone-info')} spacing="none">
|
||||
<Text>User's time</Text>
|
||||
<Text>{`${userMoment.tz(user.timezone).format('DD MMM, HH:mm')}`}</Text>
|
||||
<Text>User's local time</Text>
|
||||
<Text>{`${getCurrentDateInTimezone(user.timezone).format('DD MMM, HH:mm')}`}</Text>
|
||||
<Text>({userOffsetHoursStr})</Text>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
|
|
@ -145,6 +152,6 @@ const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (props) => {
|
|||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default ScheduleUserDetails;
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
import { Dayjs } from 'dayjs';
|
||||
|
||||
export const calculateTimePassedInDayPercentage = (date: Dayjs) => {
|
||||
const midnight = date.startOf('day');
|
||||
return (date.diff(midnight, 'minutes') / 1_440) * 100;
|
||||
};
|
||||
|
|
@ -1,31 +1,31 @@
|
|||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { HorizontalGroup, Icon, Tooltip } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { sortBy } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import ScheduleBorderedAvatar from 'components/ScheduleBorderedAvatar/ScheduleBorderedAvatar';
|
||||
import ScheduleUserDetails from 'components/ScheduleUserDetails/ScheduleUserDetails';
|
||||
import Text from 'components/Text/Text';
|
||||
import WorkingHours from 'components/WorkingHours/WorkingHours';
|
||||
import { IsOncallIcon } from 'icons';
|
||||
import { Schedule } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { getCurrentDateInTimezone } from 'models/timezone/timezone.helpers';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { getColorSchemeMappingForUsers } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import ScheduleUserDetails from './ScheduleUserDetails/ScheduleUserDetails';
|
||||
import { calculateTimePassedInDayPercentage } from './UsersTimezones.helpers';
|
||||
|
||||
import styles from './UsersTimezones.module.css';
|
||||
|
||||
interface UsersTimezonesProps {
|
||||
userIds: Array<User['pk']>;
|
||||
tz: Timezone;
|
||||
onCallNow: Array<Partial<User>>;
|
||||
scheduleId: Schedule['id'];
|
||||
startMoment: dayjs.Dayjs;
|
||||
|
||||
onTzChange: (tz: Timezone) => void;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -34,11 +34,14 @@ const hoursToSplit = 3;
|
|||
|
||||
const jLimit = 24 / hoursToSplit;
|
||||
|
||||
const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
||||
const UsersTimezones: FC<UsersTimezonesProps> = observer((props) => {
|
||||
const store = useStore();
|
||||
const { userStore } = store;
|
||||
const {
|
||||
userStore,
|
||||
timezoneStore: { selectedTimezoneLabel, currentDateInSelectedTimezone },
|
||||
} = store;
|
||||
|
||||
const { userIds, tz, onTzChange, onCallNow, scheduleId, startMoment } = props;
|
||||
const { userIds, onCallNow, scheduleId } = props;
|
||||
|
||||
useEffect(() => {
|
||||
userIds.forEach((userId) => {
|
||||
|
|
@ -53,15 +56,6 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
[userIds, store.userStore.items]
|
||||
);
|
||||
|
||||
const currentMoment = useMemo(() => dayjs().tz(tz), [tz]);
|
||||
|
||||
const currentTimeX = useMemo(() => {
|
||||
const midnight = dayjs().tz(tz).startOf('day');
|
||||
const diff = currentMoment.diff(midnight, 'minutes');
|
||||
|
||||
return (diff / 1440) * 100;
|
||||
}, [currentMoment, tz]);
|
||||
|
||||
const momentsToRender = useMemo(() => {
|
||||
const momentsToRender = [];
|
||||
|
||||
|
|
@ -78,14 +72,12 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
<div className={cx('root')}>
|
||||
<WorkingHours
|
||||
light
|
||||
startMoment={currentMoment.startOf('day')}
|
||||
startMoment={currentDateInSelectedTimezone.startOf('day')}
|
||||
duration={24 * 60 * 60}
|
||||
timezone={userStore.currentUser?.timezone}
|
||||
workingHours={userStore.currentUser?.working_hours}
|
||||
className={cx('working-hours')}
|
||||
/>
|
||||
{/* <div className={cx('shades', 'shades--left')} />
|
||||
<div className={cx('shades', 'shades--right')} /> */}
|
||||
<div className={cx('content')}>
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
|
|
@ -98,20 +90,18 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
</HorizontalGroup>
|
||||
<div className={cx('timezone-select')}>
|
||||
<Text type="secondary">
|
||||
Current timezone: {tz}, local time: {currentMoment.format('HH:mm')}
|
||||
Current timezone: {selectedTimezoneLabel}, local time: {currentDateInSelectedTimezone.format('HH:mm')}
|
||||
</Text>
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('users')}>
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX}%` }} />
|
||||
{users && users.length ? (
|
||||
<CurrentTimeLineIndicator />
|
||||
{users?.length ? (
|
||||
<UserAvatars
|
||||
users={users}
|
||||
onCallNow={onCallNow}
|
||||
onTzChange={onTzChange}
|
||||
currentMoment={currentMoment}
|
||||
startMoment={startMoment}
|
||||
currentMoment={currentDateInSelectedTimezone}
|
||||
scheduleId={scheduleId}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -150,70 +140,66 @@ const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const CurrentTimeLineIndicator = observer(() => {
|
||||
const {
|
||||
timezoneStore: { currentDateInSelectedTimezone },
|
||||
} = useStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('current-time')}
|
||||
style={{ left: `${calculateTimePassedInDayPercentage(currentDateInSelectedTimezone)}%` }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
interface UserAvatarsProps {
|
||||
users: User[];
|
||||
currentMoment: dayjs.Dayjs;
|
||||
startMoment: dayjs.Dayjs;
|
||||
scheduleId: Schedule['id'];
|
||||
onTzChange: (timezone: Timezone) => void;
|
||||
onCallNow: Array<Partial<User>>;
|
||||
}
|
||||
|
||||
const UserAvatars = (props: UserAvatarsProps) => {
|
||||
const { users, currentMoment, onCallNow, scheduleId, startMoment } = props;
|
||||
const userGroups = useMemo(() => {
|
||||
return users
|
||||
.reduce((memo, user) => {
|
||||
const userUtcOffset = dayjs().tz(user.timezone).utcOffset();
|
||||
let group = memo.find((group) => group.utcOffset === userUtcOffset);
|
||||
if (!group) {
|
||||
group = { utcOffset: userUtcOffset, users: [] };
|
||||
memo.push(group);
|
||||
}
|
||||
group.users.push(user);
|
||||
const { users, currentMoment, onCallNow, scheduleId } = props;
|
||||
const userGroups = useMemo(
|
||||
() =>
|
||||
sortBy(
|
||||
users.reduce((memo, user) => {
|
||||
const userUtcOffset = dayjs().tz(user.timezone).utcOffset();
|
||||
let group = memo.find((group) => group.utcOffset === userUtcOffset);
|
||||
if (!group) {
|
||||
group = { utcOffset: userUtcOffset, users: [] };
|
||||
memo.push(group);
|
||||
}
|
||||
group.users.push(user);
|
||||
|
||||
return memo;
|
||||
}, [])
|
||||
.sort((a, b) => {
|
||||
if (a.utcOffset > b.utcOffset) {
|
||||
return 1;
|
||||
}
|
||||
if (a.utcOffset < b.utcOffset) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}, [users]);
|
||||
return memo;
|
||||
}, []),
|
||||
({ utcOffset }) => utcOffset
|
||||
),
|
||||
[users]
|
||||
);
|
||||
|
||||
const [activeUtcOffset, setActiveUtcOffset] = useState<number | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<div className={cx('user-avatars')}>
|
||||
{userGroups.map((group, idx) => {
|
||||
const userCurrentMoment = dayjs(currentMoment).tz(group.users[0].timezone); // TODO try using group.utcOffset
|
||||
const diff = userCurrentMoment.diff(userCurrentMoment.startOf('day'), 'minutes');
|
||||
|
||||
const xPos = (diff / (60 * 24)) * 100;
|
||||
|
||||
return (
|
||||
<AvatarGroup
|
||||
key={idx}
|
||||
activeUtcOffset={activeUtcOffset}
|
||||
utcOffset={group.utcOffset}
|
||||
onSetActiveUtcOffset={setActiveUtcOffset}
|
||||
// onTzChange={onTzChange}
|
||||
xPos={xPos}
|
||||
users={group.users}
|
||||
startMoment={startMoment}
|
||||
currentMoment={currentMoment}
|
||||
scheduleId={scheduleId}
|
||||
onCallNow={onCallNow}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{userGroups.map((group, idx) => (
|
||||
<AvatarGroup
|
||||
key={idx}
|
||||
activeUtcOffset={activeUtcOffset}
|
||||
utcOffset={group.utcOffset}
|
||||
onSetActiveUtcOffset={setActiveUtcOffset}
|
||||
xPos={calculateTimePassedInDayPercentage(getCurrentDateInTimezone(group.users[0].timezone))}
|
||||
users={group.users}
|
||||
currentMoment={currentMoment}
|
||||
scheduleId={scheduleId}
|
||||
onCallNow={onCallNow}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -221,13 +207,11 @@ const UserAvatars = (props: UserAvatarsProps) => {
|
|||
interface AvatarGroupProps {
|
||||
users: User[];
|
||||
xPos: number;
|
||||
startMoment: dayjs.Dayjs;
|
||||
currentMoment: dayjs.Dayjs;
|
||||
utcOffset: number;
|
||||
scheduleId: Schedule['id'];
|
||||
onSetActiveUtcOffset: (utcOffset: number | undefined) => void;
|
||||
activeUtcOffset: number;
|
||||
onTzChange?: (timezone: Timezone) => void;
|
||||
onCallNow: Array<Partial<User>>;
|
||||
}
|
||||
|
||||
|
|
@ -235,18 +219,16 @@ const LIMIT = 3;
|
|||
const AVATAR_WIDTH = 32;
|
||||
const AVATAR_GAP = 5;
|
||||
|
||||
const AvatarGroup = (props: AvatarGroupProps) => {
|
||||
const AvatarGroup = observer((props: AvatarGroupProps) => {
|
||||
const {
|
||||
users: propsUsers,
|
||||
currentMoment,
|
||||
xPos,
|
||||
onTzChange,
|
||||
utcOffset,
|
||||
onSetActiveUtcOffset,
|
||||
activeUtcOffset,
|
||||
onCallNow,
|
||||
scheduleId,
|
||||
startMoment,
|
||||
} = props;
|
||||
|
||||
const store = useStore();
|
||||
|
|
@ -271,16 +253,7 @@ const AvatarGroup = (props: AvatarGroupProps) => {
|
|||
});
|
||||
}, [propsUsers, onCallNow]);
|
||||
|
||||
const getAvatarClickHandler = useCallback(
|
||||
(timezone: Timezone) => {
|
||||
return () => {
|
||||
onTzChange(timezone);
|
||||
};
|
||||
},
|
||||
[onTzChange]
|
||||
);
|
||||
|
||||
const colorSchemeMapping = getColorSchemeMappingForUsers(store, scheduleId, startMoment);
|
||||
const colorSchemeMapping = getColorSchemeMappingForUsers(store, scheduleId, store.timezoneStore.calendarStartDate);
|
||||
const width = active ? users.length * AVATAR_WIDTH + (users.length - 1) * AVATAR_GAP : AVATAR_WIDTH;
|
||||
|
||||
return (
|
||||
|
|
@ -307,19 +280,18 @@ const AvatarGroup = (props: AvatarGroupProps) => {
|
|||
user={user}
|
||||
isOncall={isOncall}
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cx('avatar')}
|
||||
data-testid="user-avatar-in-schedule"
|
||||
style={{
|
||||
left: active ? `${index * (AVATAR_WIDTH + AVATAR_GAP)}px` : `${index * 10}px`,
|
||||
opacity: active ? 1 : Math.max(1 - index * 0.25, 0.25),
|
||||
visibility: !active && index >= LIMIT ? 'hidden' : 'visible',
|
||||
zIndex: array.length - index - 1,
|
||||
}}
|
||||
onClick={getAvatarClickHandler(user.timezone)}
|
||||
>
|
||||
<ScheduleBorderedAvatar
|
||||
colors={colorSchemeList}
|
||||
|
|
@ -329,7 +301,7 @@ const AvatarGroup = (props: AvatarGroupProps) => {
|
|||
renderIcon={() =>
|
||||
isOncall ? <IsOncallIcon className={cx('is-oncall-icon')} width={14} height={13} /> : null
|
||||
}
|
||||
></ScheduleBorderedAvatar>
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
@ -346,6 +318,6 @@ const AvatarGroup = (props: AvatarGroupProps) => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default UsersTimezones;
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ export enum ActionKey {
|
|||
ADD_NEW_COLUMN_TO_ALERT_GROUP = 'ADD_NEW_COLUMN_TO_ALERT_GROUP',
|
||||
REMOVE_COLUMN_FROM_ALERT_GROUP = 'REMOVE_COLUMN_FROM_ALERT_GROUP',
|
||||
RESET_COLUMNS_FROM_ALERT_GROUP = 'RESET_COLUMNS_FROM_ALERT_GROUP',
|
||||
UPDATE_PERSONAL_EVENTS = 'UPDATE_PERSONAL_EVENTS',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { action, makeObservable, observable, runInAction } from 'mobx';
|
||||
|
||||
import { PageErrorData } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import { getWrongTeamResponseInfo } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import { RemoteFiltersType } from 'containers/RemoteFilters/RemoteFilters.types';
|
||||
import BaseStore from 'models/base_store';
|
||||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||
import { ActionKey } from 'models/loader/action-keys';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { makeRequest } from 'network';
|
||||
import { RootStore } from 'state';
|
||||
import { SelectOption } from 'state/types';
|
||||
import { AutoLoadingState } from 'utils/decorators';
|
||||
|
||||
import {
|
||||
createShiftSwapEventFromShiftSwap,
|
||||
|
|
@ -42,6 +46,9 @@ export class ScheduleStore extends BaseStore {
|
|||
@observable.shallow
|
||||
items: { [id: string]: Schedule } = {};
|
||||
|
||||
@observable
|
||||
quality: ScheduleScoreQualityResponse;
|
||||
|
||||
@observable.shallow
|
||||
shifts: { [id: string]: Shift } = {};
|
||||
|
||||
|
|
@ -113,7 +120,10 @@ export class ScheduleStore extends BaseStore {
|
|||
byDayOptions: SelectOption[] = [];
|
||||
|
||||
@observable
|
||||
scheduleId: Schedule['id'];
|
||||
refreshEventsError?: Partial<PageErrorData> = {
|
||||
isWrongTeamError: false,
|
||||
wrongTeamNoPermissions: false,
|
||||
};
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore);
|
||||
|
|
@ -209,8 +219,15 @@ export class ScheduleStore extends BaseStore {
|
|||
};
|
||||
}
|
||||
|
||||
async getScoreQuality(scheduleId: Schedule['id']): Promise<ScheduleScoreQualityResponse> {
|
||||
return await makeRequest(`/schedules/${scheduleId}/quality`, { method: 'GET' });
|
||||
@action.bound
|
||||
async getScoreQuality(scheduleId: Schedule['id']) {
|
||||
const [quality] = await Promise.all([
|
||||
makeRequest(`/schedules/${scheduleId}/quality`, { method: 'GET' }),
|
||||
this.updateRelatedEscalationChains(scheduleId),
|
||||
]);
|
||||
runInAction(() => {
|
||||
this.quality = quality;
|
||||
});
|
||||
}
|
||||
|
||||
async reloadIcal(scheduleId: Schedule['id']) {
|
||||
|
|
@ -247,6 +264,8 @@ export class ScheduleStore extends BaseStore {
|
|||
data: { type, schedule: scheduleId, ...params },
|
||||
method: 'POST',
|
||||
});
|
||||
await this.rootStore.scheduleStore.refreshEvents(scheduleId);
|
||||
await this.getScoreQuality(scheduleId);
|
||||
|
||||
runInAction(() => {
|
||||
this.shifts = {
|
||||
|
|
@ -506,6 +525,29 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
async refreshEvents(scheduleId: string) {
|
||||
this.refreshEventsError = {};
|
||||
const startMoment = this.rootStore.timezoneStore.calendarStartDate;
|
||||
|
||||
try {
|
||||
const schedule = await this.loadItem(scheduleId);
|
||||
this.rootStore.setPageTitle(schedule?.name);
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.refreshEventsError = getWrongTeamResponseInfo(error);
|
||||
});
|
||||
}
|
||||
|
||||
this.updateRelatedUsers(scheduleId); // to refresh related users
|
||||
await Promise.all([
|
||||
this.updateEvents(scheduleId, startMoment, 'rotation'),
|
||||
this.updateEvents(scheduleId, startMoment, 'override'),
|
||||
this.updateEvents(scheduleId, startMoment, 'final'),
|
||||
this.updateShiftSwaps(scheduleId, startMoment),
|
||||
]);
|
||||
}
|
||||
|
||||
async updateFrequencyOptions() {
|
||||
return await makeRequest(`/oncall_shifts/frequency_options/`, {
|
||||
method: 'GET',
|
||||
|
|
@ -587,6 +629,7 @@ export class ScheduleStore extends BaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@AutoLoadingState(ActionKey.UPDATE_PERSONAL_EVENTS)
|
||||
@action
|
||||
async updatePersonalEvents(userPk: User['pk'], startMoment: dayjs.Dayjs, days = 9, isUpdateOnCallNow = false) {
|
||||
const fromString = getFromString(startMoment);
|
||||
|
|
|
|||
|
|
@ -595,11 +595,26 @@ export const allTimezones = [
|
|||
'Zulu',
|
||||
];
|
||||
|
||||
export const getTzOffsetString = (moment: dayjs.Dayjs) => {
|
||||
const userOffset = moment.utcOffset();
|
||||
const userOffsetHours = userOffset / 60;
|
||||
const userOffsetHoursStr =
|
||||
userOffsetHours > 0 ? `+${userOffsetHours} GMT` : userOffset < 0 ? `${userOffsetHours} GMT` : `GMT`;
|
||||
// TODO: move it to utils
|
||||
|
||||
return userOffsetHoursStr;
|
||||
export const getTzOffsetString = (date: dayjs.Dayjs) => {
|
||||
const userOffset = date.utcOffset();
|
||||
const userOffsetHours = userOffset / 60;
|
||||
|
||||
if (userOffsetHours === 0) {
|
||||
return 'GMT';
|
||||
}
|
||||
return userOffsetHours > 0 ? `GMT+${userOffsetHours}` : `GMT${userOffsetHours}`;
|
||||
};
|
||||
|
||||
export const getGMTTimezoneLabelBasedOnOffset = (offset: number) => {
|
||||
const userOffsetHours = offset / 60;
|
||||
if (userOffsetHours === 0) {
|
||||
return 'GMT';
|
||||
}
|
||||
return `GMT${userOffsetHours >= 0 ? '+' : ''}${userOffsetHours}`;
|
||||
};
|
||||
|
||||
export const getCurrentlyLoggedInUserDate = () => dayjs();
|
||||
export const getOffsetOfCurrentUser = () => dayjs().utcOffset();
|
||||
export const getCurrentDateInTimezone = (timezone: string) => dayjs().tz(timezone);
|
||||
|
|
|
|||
58
grafana-plugin/src/models/timezone/timezone.ts
Normal file
58
grafana-plugin/src/models/timezone/timezone.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { observable, action, computed, makeObservable } from 'mobx';
|
||||
|
||||
// TODO: move utils from Schedule.helpers to common place
|
||||
import { getStartOfWeekBasedOnCurrentDate } from 'pages/schedule/Schedule.helpers';
|
||||
import { RootStore } from 'state';
|
||||
|
||||
import { getOffsetOfCurrentUser, getGMTTimezoneLabelBasedOnOffset } from './timezone.helpers';
|
||||
|
||||
export class TimezoneStore {
|
||||
constructor(rootStore: RootStore) {
|
||||
makeObservable(this);
|
||||
|
||||
this.rootStore = rootStore;
|
||||
}
|
||||
|
||||
rootStore: RootStore;
|
||||
|
||||
@observable
|
||||
selectedTimezoneOffset = getOffsetOfCurrentUser();
|
||||
|
||||
@observable
|
||||
calendarStartDate = getStartOfWeekBasedOnCurrentDate(this.currentDateInSelectedTimezone);
|
||||
|
||||
@action.bound
|
||||
async setSelectedTimezoneOffset(offset: number) {
|
||||
this.selectedTimezoneOffset = offset;
|
||||
this.calendarStartDate = getStartOfWeekBasedOnCurrentDate(this.currentDateInSelectedTimezone);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
setCalendarStartDate(date: Dayjs) {
|
||||
this.calendarStartDate = date;
|
||||
}
|
||||
|
||||
@action.bound
|
||||
setSelectedTimezoneOffsetBasedOnTz(timezone: string) {
|
||||
this.selectedTimezoneOffset = dayjs().tz(timezone).utcOffset();
|
||||
}
|
||||
|
||||
@action.bound
|
||||
getDateInSelectedTimezone(date: Dayjs | string) {
|
||||
if (typeof date === 'string') {
|
||||
date = dayjs(date);
|
||||
}
|
||||
return dayjs(date.format()).utcOffset(this.selectedTimezoneOffset);
|
||||
}
|
||||
|
||||
@computed
|
||||
get selectedTimezoneLabel() {
|
||||
return getGMTTimezoneLabelBasedOnOffset(this.selectedTimezoneOffset);
|
||||
}
|
||||
|
||||
@computed
|
||||
get currentDateInSelectedTimezone() {
|
||||
return dayjs().utcOffset(this.selectedTimezoneOffset);
|
||||
}
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ export class UserStore extends BaseStore {
|
|||
this.update(id, { timezone });
|
||||
}
|
||||
|
||||
this.rootStore.currentTimezone = timezone;
|
||||
this.rootStore.timezoneStore.setSelectedTimezoneOffsetBasedOnTz(timezone);
|
||||
|
||||
return timezone;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,10 +70,7 @@ import styles from './Incident.module.scss';
|
|||
const cx = cn.bind(styles);
|
||||
const INTEGRATION_NAME_LENGTH_LIMIT = 30;
|
||||
|
||||
interface IncidentPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {
|
||||
pageTitle: string;
|
||||
setPageTitle: (value: string) => void;
|
||||
}
|
||||
interface IncidentPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
|
||||
|
||||
interface IncidentPageState extends PageBaseState {
|
||||
showIntegrationSettings?: boolean;
|
||||
|
|
@ -99,9 +96,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
const { setPageTitle } = this.props;
|
||||
|
||||
setPageTitle(undefined);
|
||||
this.props.store.setPageTitle('');
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IncidentPageProps) {
|
||||
|
|
@ -118,13 +113,12 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
match: {
|
||||
params: { id },
|
||||
},
|
||||
setPageTitle,
|
||||
} = this.props;
|
||||
|
||||
store.alertGroupStore
|
||||
.getAlert(id)
|
||||
.then((alertGroup) => {
|
||||
setPageTitle(`#${alertGroup.inside_organization_number} ${alertGroup.render_for_web.title}`);
|
||||
store.setPageTitle(`#${alertGroup.inside_organization_number} ${alertGroup.render_for_web.title}`);
|
||||
})
|
||||
.catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } }));
|
||||
};
|
||||
|
|
@ -262,7 +256,6 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
match: {
|
||||
params: { id },
|
||||
},
|
||||
pageTitle,
|
||||
} = this.props;
|
||||
|
||||
const { alerts } = store.alertGroupStore;
|
||||
|
|
@ -288,7 +281,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
{/* @ts-ignore*/}
|
||||
<HorizontalGroup align="baseline">
|
||||
<Text.Title level={3} data-testid="incident-title">
|
||||
{pageTitle}
|
||||
{store.pageTitle}
|
||||
</Text.Title>
|
||||
{incident.root_alert_group && (
|
||||
<Text type="secondary">
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const mondayDayOffset = {
|
|||
};
|
||||
|
||||
export const getWeekStartString = () => {
|
||||
const weekStart = (config.bootData.user.weekStart || '').toLowerCase();
|
||||
const weekStart = (config?.bootData?.user?.weekStart || '').toLowerCase();
|
||||
|
||||
if (!weekStart || weekStart === 'browser') {
|
||||
return 'monday';
|
||||
|
|
@ -40,6 +40,12 @@ export const getStartOfWeek = (tz: Timezone) => {
|
|||
.add(mondayDayOffset[getWeekStartString()], 'day');
|
||||
};
|
||||
|
||||
export const getStartOfWeekBasedOnCurrentDate = (date: dayjs.Dayjs) => {
|
||||
return date
|
||||
.startOf('isoWeek') // it's Monday always
|
||||
.add(mondayDayOffset[getWeekStartString()], 'day');
|
||||
};
|
||||
|
||||
export const getUTCString = (moment: dayjs.Dayjs) => {
|
||||
return moment.utc().format('YYYY-MM-DDTHH:mm:ss.000Z');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,17 +6,12 @@ import dayjs from 'dayjs';
|
|||
import { observer } from 'mobx-react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import {
|
||||
getWrongTeamResponseInfo,
|
||||
initErrorDataState,
|
||||
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import PageErrorHandlingWrapper from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import ScheduleFilters from 'components/ScheduleFilters/ScheduleFilters';
|
||||
import { ScheduleFiltersType } from 'components/ScheduleFilters/ScheduleFilters.types';
|
||||
import ScheduleQuality from 'components/ScheduleQuality/ScheduleQuality';
|
||||
import Text from 'components/Text/Text';
|
||||
import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import ShiftSwapForm from 'containers/RotationForm/ShiftSwapForm';
|
||||
import Rotations from 'containers/Rotations/Rotations';
|
||||
|
|
@ -24,28 +19,24 @@ import ScheduleFinal from 'containers/Rotations/ScheduleFinal';
|
|||
import ScheduleOverrides from 'containers/Rotations/ScheduleOverrides';
|
||||
import ScheduleForm from 'containers/ScheduleForm/ScheduleForm';
|
||||
import ScheduleICalSettings from 'containers/ScheduleIcalLink/ScheduleIcalLink';
|
||||
import UserTimezoneSelect from 'containers/UserTimezoneSelect/UserTimezoneSelect';
|
||||
import UsersTimezones from 'containers/UsersTimezones/UsersTimezones';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { Event, Schedule, ScheduleType, Shift, ShiftSwap } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { isUserActionAllowed, UserActions } from 'utils/authorization';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import { getStartOfWeek } from './Schedule.helpers';
|
||||
import { getStartOfWeekBasedOnCurrentDate } from './Schedule.helpers';
|
||||
|
||||
import styles from './Schedule.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface SchedulePageProps extends PageProps, WithStoreProps, RouteComponentProps<{ id: string }> {
|
||||
pageTitle: string;
|
||||
setPageTitle: (value: string) => void;
|
||||
}
|
||||
interface SchedulePageProps extends PageProps, WithStoreProps, RouteComponentProps<{ id: string }> {}
|
||||
|
||||
interface SchedulePageState extends PageBaseState {
|
||||
startMoment: dayjs.Dayjs;
|
||||
interface SchedulePageState {
|
||||
schedulePeriodType: string;
|
||||
renderType: string;
|
||||
shiftIdToShowRotationForm?: Shift['id'];
|
||||
|
|
@ -64,13 +55,12 @@ interface SchedulePageState extends PageBaseState {
|
|||
@observer
|
||||
class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState> {
|
||||
highlightMyShiftsWasToggled = false;
|
||||
scheduleId = this.props.match.params.id;
|
||||
|
||||
constructor(props: SchedulePageProps) {
|
||||
super(props);
|
||||
|
||||
const { store } = this.props;
|
||||
this.state = {
|
||||
startMoment: getStartOfWeek(store.currentTimezone),
|
||||
schedulePeriodType: 'week',
|
||||
renderType: 'timeline',
|
||||
shiftIdToShowRotationForm: undefined,
|
||||
|
|
@ -78,56 +68,44 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
isLoading: true,
|
||||
showEditForm: false,
|
||||
showScheduleICalSettings: false,
|
||||
errorData: initErrorDataState(),
|
||||
lastUpdated: 0,
|
||||
filters: { users: [] },
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
store,
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
const { store } = this.props;
|
||||
|
||||
store.userStore.updateItems();
|
||||
|
||||
store.scheduleStore.updateFrequencyOptions();
|
||||
store.scheduleStore.updateDaysOptions();
|
||||
await store.scheduleStore.updateOncallShifts(id); // TODO we should know shifts to render Rotations
|
||||
await this.updateEvents();
|
||||
await store.scheduleStore.updateOncallShifts(this.scheduleId); // TODO we should know shifts to render Rotations
|
||||
await store.scheduleStore.refreshEvents(this.scheduleId);
|
||||
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { store, setPageTitle } = this.props;
|
||||
|
||||
const { store } = this.props;
|
||||
store.scheduleStore.clearPreview();
|
||||
|
||||
setPageTitle(undefined);
|
||||
store.setPageTitle(undefined);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
store,
|
||||
query,
|
||||
pageTitle,
|
||||
match: {
|
||||
params: { id: scheduleId },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
startMoment,
|
||||
|
||||
shiftIdToShowRotationForm,
|
||||
shiftIdToShowOverridesForm,
|
||||
showEditForm,
|
||||
showScheduleICalSettings,
|
||||
errorData,
|
||||
shiftStartToShowOverrideForm,
|
||||
shiftEndToShowOverrideForm,
|
||||
filters,
|
||||
|
|
@ -135,9 +113,9 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
shiftSwapParamsToShowForm,
|
||||
} = this.state;
|
||||
|
||||
const { isNotFoundError } = errorData;
|
||||
const { isNotFoundError } = store.scheduleStore.refreshEventsError;
|
||||
|
||||
const { scheduleStore, currentTimezone } = store;
|
||||
const { scheduleStore } = store;
|
||||
|
||||
const users = store.userStore.getSearchResult().results;
|
||||
const schedule = scheduleStore.items[scheduleId];
|
||||
|
|
@ -163,7 +141,11 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
shiftSwapIdToShowForm;
|
||||
|
||||
return (
|
||||
<PageErrorHandlingWrapper errorData={errorData} objectName="schedule" pageName="schedules">
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={store.scheduleStore.refreshEventsError}
|
||||
objectName="schedule"
|
||||
pageName="schedules"
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
|
|
@ -193,19 +175,15 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
level={2}
|
||||
onTextChange={this.handleNameChange}
|
||||
>
|
||||
{pageTitle}
|
||||
{store.pageTitle}
|
||||
</Text.Title>
|
||||
{schedule && <ScheduleQuality schedule={schedule} lastUpdated={this.state.lastUpdated} />}
|
||||
{schedule && <ScheduleQuality schedule={schedule} />}
|
||||
</div>
|
||||
<HorizontalGroup spacing="lg">
|
||||
{users && (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Current timezone:</Text>
|
||||
<UserTimezoneSelect
|
||||
value={currentTimezone}
|
||||
users={users}
|
||||
onChange={this.handleTimezoneChange}
|
||||
/>
|
||||
<UserTimezoneSelect scheduleId={scheduleId} />
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
|
|
@ -241,15 +219,12 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
<div className={cx('users-timezones')}>
|
||||
<UsersTimezones
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
onCallNow={schedule?.on_call_now || []}
|
||||
userIds={
|
||||
scheduleStore.relatedUsers[scheduleId]
|
||||
? Object.keys(scheduleStore.relatedUsers[scheduleId])
|
||||
: []
|
||||
}
|
||||
tz={currentTimezone}
|
||||
onTzChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -269,7 +244,8 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
</Button>
|
||||
</HorizontalGroup>
|
||||
<Text.Title style={{ marginLeft: '8px' }} level={4} type="primary">
|
||||
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
|
||||
{store.timezoneStore.calendarStartDate.format('DD MMM')} -{' '}
|
||||
{store.timezoneStore.calendarStartDate.add(6, 'day').format('DD MMM')}
|
||||
</Text.Title>
|
||||
</HorizontalGroup>
|
||||
<ScheduleFilters
|
||||
|
|
@ -281,8 +257,6 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
</div>
|
||||
<ScheduleFinal
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
disabled={disabledRotationForm}
|
||||
onShowOverrideForm={this.handleShowOverridesForm}
|
||||
filters={filters}
|
||||
|
|
@ -303,8 +277,6 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
/>
|
||||
<Rotations
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateRotation}
|
||||
onUpdate={this.handleUpdateRotation}
|
||||
onDelete={this.handleDeleteRotation}
|
||||
|
|
@ -318,8 +290,6 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
/>
|
||||
<ScheduleOverrides
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateOverride}
|
||||
onUpdate={this.handleUpdateOverride}
|
||||
onDelete={this.handleDeleteOverride}
|
||||
|
|
@ -359,8 +329,6 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
<ShiftSwapForm
|
||||
id={shiftSwapIdToShowForm}
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
currentTimezone={currentTimezone}
|
||||
params={shiftSwapParamsToShowForm}
|
||||
onHide={this.handleHideShiftSwapForm}
|
||||
onUpdate={this.handleUpdateShiftSwaps}
|
||||
|
|
@ -378,13 +346,12 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
match: {
|
||||
params: { id: scheduleId },
|
||||
},
|
||||
setPageTitle,
|
||||
} = this.props;
|
||||
|
||||
const { scheduleStore } = store;
|
||||
|
||||
return scheduleStore.loadItem(scheduleId).then((schedule) => {
|
||||
setPageTitle(schedule?.name);
|
||||
store.setPageTitle(schedule?.name);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -406,7 +373,6 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
match: {
|
||||
params: { id: scheduleId },
|
||||
},
|
||||
setPageTitle,
|
||||
} = this.props;
|
||||
|
||||
const schedule = store.scheduleStore.items[scheduleId];
|
||||
|
|
@ -415,46 +381,14 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
.update(scheduleId, { type: schedule.type, name: value })
|
||||
.then(() => store.scheduleStore.loadItem(scheduleId))
|
||||
.then((schedule) => {
|
||||
setPageTitle(schedule?.name);
|
||||
store.setPageTitle(schedule?.name);
|
||||
});
|
||||
};
|
||||
|
||||
updateEvents = () => {
|
||||
const {
|
||||
store,
|
||||
match: {
|
||||
params: { id: scheduleId },
|
||||
},
|
||||
setPageTitle,
|
||||
} = this.props;
|
||||
|
||||
const { startMoment } = this.state;
|
||||
|
||||
this.setState((prevState) => ({
|
||||
// this will update schedule score
|
||||
lastUpdated: prevState.lastUpdated + 1,
|
||||
}));
|
||||
|
||||
store.scheduleStore
|
||||
.loadItem(scheduleId) // to refresh current oncall users
|
||||
.then((schedule) => {
|
||||
setPageTitle(schedule?.name);
|
||||
})
|
||||
.catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } }));
|
||||
store.scheduleStore.updateRelatedUsers(scheduleId); // to refresh related users
|
||||
|
||||
return Promise.all([
|
||||
store.scheduleStore.updateEvents(scheduleId, startMoment, 'rotation'),
|
||||
store.scheduleStore.updateEvents(scheduleId, startMoment, 'override'),
|
||||
store.scheduleStore.updateEvents(scheduleId, startMoment, 'final'),
|
||||
store.scheduleStore.updateShiftSwaps(scheduleId, startMoment),
|
||||
]);
|
||||
};
|
||||
|
||||
handleCreateRotation = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.updateEvents().then(() => {
|
||||
store.scheduleStore.refreshEvents(this.scheduleId).then(() => {
|
||||
store.scheduleStore.clearPreview();
|
||||
});
|
||||
};
|
||||
|
|
@ -462,7 +396,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
handleCreateOverride = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.updateEvents().then(() => {
|
||||
store.scheduleStore.refreshEvents(this.scheduleId).then(() => {
|
||||
store.scheduleStore.clearPreview();
|
||||
});
|
||||
};
|
||||
|
|
@ -470,7 +404,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
handleUpdateRotation = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.updateEvents().then(() => {
|
||||
store.scheduleStore.refreshEvents(this.scheduleId).then(() => {
|
||||
store.scheduleStore.clearPreview();
|
||||
});
|
||||
};
|
||||
|
|
@ -478,7 +412,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
handleUpdateShiftSwaps = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.updateEvents().then(() => {
|
||||
store.scheduleStore.refreshEvents(this.scheduleId).then(() => {
|
||||
store.scheduleStore.clearPreview();
|
||||
});
|
||||
};
|
||||
|
|
@ -486,7 +420,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
handleDeleteRotation = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.updateEvents().then(() => {
|
||||
store.scheduleStore.refreshEvents(this.scheduleId).then(() => {
|
||||
store.scheduleStore.clearPreview();
|
||||
});
|
||||
};
|
||||
|
|
@ -494,7 +428,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
handleDeleteOverride = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.updateEvents().then(() => {
|
||||
store.scheduleStore.refreshEvents(this.scheduleId).then(() => {
|
||||
store.scheduleStore.clearPreview();
|
||||
});
|
||||
};
|
||||
|
|
@ -502,25 +436,11 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
handleUpdateOverride = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
this.updateEvents().then(() => {
|
||||
store.scheduleStore.refreshEvents(this.scheduleId).then(() => {
|
||||
store.scheduleStore.clearPreview();
|
||||
});
|
||||
};
|
||||
|
||||
handleTimezoneChange = (value: Timezone) => {
|
||||
const { store } = this.props;
|
||||
|
||||
const oldTimezone = store.currentTimezone;
|
||||
|
||||
this.setState((oldState) => {
|
||||
const wDiff = oldState.startMoment.diff(getStartOfWeek(oldTimezone), 'weeks');
|
||||
|
||||
return { ...oldState, startMoment: getStartOfWeek(value).add(wDiff, 'weeks') };
|
||||
}, this.updateEvents);
|
||||
|
||||
store.currentTimezone = value;
|
||||
};
|
||||
|
||||
handleShedulePeriodTypeChange = (value: string) => {
|
||||
this.setState({ schedulePeriodType: value });
|
||||
};
|
||||
|
|
@ -530,23 +450,28 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
};
|
||||
|
||||
handleDateRangeUpdate = async () => {
|
||||
await this.updateEvents();
|
||||
await this.props.store.scheduleStore.refreshEvents(this.scheduleId);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
handleTodayClick = () => {
|
||||
const { store } = this.props;
|
||||
this.setState({ startMoment: getStartOfWeek(store.currentTimezone) }, this.handleDateRangeUpdate);
|
||||
store.timezoneStore.setCalendarStartDate(
|
||||
getStartOfWeekBasedOnCurrentDate(store.timezoneStore.currentDateInSelectedTimezone)
|
||||
);
|
||||
this.handleDateRangeUpdate();
|
||||
};
|
||||
|
||||
handleLeftClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
this.setState({ startMoment: startMoment.add(-7, 'day') }, this.handleDateRangeUpdate);
|
||||
const { store } = this.props;
|
||||
store.timezoneStore.setCalendarStartDate(store.timezoneStore.calendarStartDate.subtract(7, 'day'));
|
||||
this.handleDateRangeUpdate();
|
||||
};
|
||||
|
||||
handleRightClick = () => {
|
||||
const { startMoment } = this.state;
|
||||
this.setState({ startMoment: startMoment.add(7, 'day') }, this.handleDateRangeUpdate);
|
||||
const { store } = this.props;
|
||||
store.timezoneStore.setCalendarStartDate(store.timezoneStore.calendarStartDate.add(7, 'day'));
|
||||
this.handleDateRangeUpdate();
|
||||
};
|
||||
|
||||
handleExportClick = () => {
|
||||
|
|
@ -564,7 +489,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
await scheduleStore.reloadIcal(scheduleId);
|
||||
|
||||
store.scheduleStore.updateOncallShifts(scheduleId);
|
||||
this.updateEvents();
|
||||
store.scheduleStore.refreshEvents(scheduleId);
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { SyntheticEvent } from 'react';
|
|||
|
||||
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import qs from 'query-string';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
|
@ -13,9 +12,7 @@ import PluginLink from 'components/PluginLink/PluginLink';
|
|||
import Table from 'components/Table/Table';
|
||||
import Text from 'components/Text/Text';
|
||||
import TextEllipsisTooltip from 'components/TextEllipsisTooltip/TextEllipsisTooltip';
|
||||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
|
||||
import { RemoteFiltersType } from 'containers/RemoteFilters/RemoteFilters.types';
|
||||
|
|
@ -23,11 +20,11 @@ import ScheduleFinal from 'containers/Rotations/ScheduleFinal';
|
|||
import SchedulePersonal from 'containers/Rotations/SchedulePersonal';
|
||||
import ScheduleForm from 'containers/ScheduleForm/ScheduleForm';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import TimelineMarks from 'containers/TimelineMarks/TimelineMarks';
|
||||
import UserTimezoneSelect from 'containers/UserTimezoneSelect/UserTimezoneSelect';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
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';
|
||||
import { WithStoreProps, PageProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
|
@ -41,7 +38,6 @@ const cx = cn.bind(styles);
|
|||
interface SchedulesPageProps extends WithStoreProps, RouteComponentProps, PageProps {}
|
||||
|
||||
interface SchedulesPageState {
|
||||
startMoment: dayjs.Dayjs;
|
||||
filters: RemoteFiltersType;
|
||||
showNewScheduleSelector: boolean;
|
||||
expandedRowKeys: Array<Schedule['id']>;
|
||||
|
|
@ -53,10 +49,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
constructor(props: SchedulesPageProps) {
|
||||
super(props);
|
||||
|
||||
const { store } = this.props;
|
||||
|
||||
this.state = {
|
||||
startMoment: getStartOfWeek(store.currentTimezone),
|
||||
filters: { searchTerm: '', type: undefined, used: undefined, mine: undefined },
|
||||
showNewScheduleSelector: false,
|
||||
expandedRowKeys: [],
|
||||
|
|
@ -74,14 +67,12 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
|
||||
render() {
|
||||
const { store, query } = this.props;
|
||||
const { showNewScheduleSelector, expandedRowKeys, scheduleIdToEdit, startMoment } = this.state;
|
||||
const { showNewScheduleSelector, expandedRowKeys, scheduleIdToEdit } = this.state;
|
||||
|
||||
const { results, count, page_size } = store.scheduleStore.getSearchResult();
|
||||
|
||||
const page = store.filtersStore.currentTablePageNum[PAGE.Schedules];
|
||||
|
||||
const users = store.userStore.getSearchResult().results;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
|
|
@ -89,13 +80,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>Schedules</Text.Title>
|
||||
<div className={cx('schedules__actions')}>
|
||||
{users && (
|
||||
<UserTimezoneSelect
|
||||
value={store.currentTimezone}
|
||||
users={users}
|
||||
onChange={this.handleTimezoneChange}
|
||||
/>
|
||||
)}
|
||||
<UserTimezoneSelect />
|
||||
<WithPermissionControlTooltip userAction={UserActions.SchedulesWrite}>
|
||||
<Button variant="primary" onClick={this.handleCreateScheduleClick}>
|
||||
+ New schedule
|
||||
|
|
@ -105,11 +90,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('schedule', 'schedule-personal')}>
|
||||
<SchedulePersonal
|
||||
userPk={store.userStore.currentUserPk}
|
||||
currentTimezone={store.currentTimezone}
|
||||
startMoment={startMoment}
|
||||
/>
|
||||
<SchedulePersonal userPk={store.userStore.currentUserPk} />
|
||||
</div>
|
||||
<div className={cx('schedules__filters-container')}>
|
||||
<RemoteFilters
|
||||
|
|
@ -169,14 +150,6 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
);
|
||||
}
|
||||
|
||||
handleTimezoneChange = (value: Timezone) => {
|
||||
const { store } = this.props;
|
||||
|
||||
store.currentTimezone = value;
|
||||
|
||||
this.setState({ startMoment: getStartOfWeek(value) }, this.updateEvents);
|
||||
};
|
||||
|
||||
handleCreateScheduleClick = () => {
|
||||
this.setState({ showNewScheduleSelector: true });
|
||||
};
|
||||
|
|
@ -191,45 +164,27 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
const { expandedRowKeys } = this.state;
|
||||
|
||||
if (expanded && !expandedRowKeys.includes(data.id)) {
|
||||
this.setState({ expandedRowKeys: [...this.state.expandedRowKeys, data.id] }, this.updateEvents);
|
||||
this.setState({ expandedRowKeys: [...this.state.expandedRowKeys, data.id] }, () => {
|
||||
this.props.store.scheduleStore.refreshEvents(data.id);
|
||||
});
|
||||
} else if (!expanded && expandedRowKeys.includes(data.id)) {
|
||||
const index = expandedRowKeys.indexOf(data.id);
|
||||
const newExpandedRowKeys = [...expandedRowKeys];
|
||||
newExpandedRowKeys.splice(index, 1);
|
||||
this.setState({ expandedRowKeys: newExpandedRowKeys }, this.updateEvents);
|
||||
this.setState({ expandedRowKeys: newExpandedRowKeys }, () => {
|
||||
this.props.store.scheduleStore.refreshEvents(data.id);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateEvents = () => {
|
||||
const { store } = this.props;
|
||||
const { expandedRowKeys, startMoment } = this.state;
|
||||
|
||||
expandedRowKeys.forEach((scheduleId) => {
|
||||
store.scheduleStore.updateEvents(scheduleId, startMoment, 'rotation');
|
||||
store.scheduleStore.updateEvents(scheduleId, startMoment, 'override');
|
||||
store.scheduleStore.updateEvents(scheduleId, startMoment, 'final');
|
||||
});
|
||||
};
|
||||
|
||||
renderSchedule = (data: Schedule) => {
|
||||
const { startMoment } = this.state;
|
||||
const { store } = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx('schedule')}>
|
||||
<TimelineMarks startMoment={startMoment} timezone={store.currentTimezone} />
|
||||
<div className={cx('rotations')}>
|
||||
<ScheduleFinal
|
||||
simplified
|
||||
scheduleId={data.id}
|
||||
currentTimezone={store.currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onSlotClick={this.getScheduleClickHandler(data.id)}
|
||||
/>
|
||||
</div>
|
||||
renderSchedule = (data: Schedule) => (
|
||||
<div className={cx('schedule')}>
|
||||
<TimelineMarks />
|
||||
<div className={cx('rotations')}>
|
||||
<ScheduleFinal simplified scheduleId={data.id} onSlotClick={this.getScheduleClickHandler(data.id)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
getScheduleClickHandler = (scheduleId: Schedule['id']) => {
|
||||
const { history, query } = this.props;
|
||||
|
|
@ -404,10 +359,14 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
|
||||
update = () => {
|
||||
const { store } = this.props;
|
||||
const { startMoment } = this.state;
|
||||
const page = store.filtersStore.currentTablePageNum[PAGE.Schedules];
|
||||
|
||||
store.scheduleStore.updatePersonalEvents(store.userStore.currentUserPk, startMoment, 9, true);
|
||||
store.scheduleStore.updatePersonalEvents(
|
||||
store.userStore.currentUserPk,
|
||||
store.timezoneStore.calendarStartDate,
|
||||
9,
|
||||
true
|
||||
);
|
||||
|
||||
// For removal we need to check if count is 1, which means we should change the page to the previous one
|
||||
const { results } = store.scheduleStore.getSearchResult();
|
||||
|
|
|
|||
|
|
@ -1,17 +1,9 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import './dayjs';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import classnames from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import isBetween from 'dayjs/plugin/isBetween';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
import { observer, Provider } from 'mobx-react';
|
||||
import Header from 'navbar/Header/Header';
|
||||
import LegacyNavTabsBar from 'navbar/LegacyNavTabsBar';
|
||||
|
|
@ -42,16 +34,6 @@ import { useStore } from 'state/useStore';
|
|||
import { isUserActionAllowed } from 'utils/authorization';
|
||||
import { DEFAULT_PAGE } from 'utils/consts';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(weekday);
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
import 'assets/style/vars.css';
|
||||
import 'assets/style/global.css';
|
||||
import 'assets/style/utils.css';
|
||||
|
|
@ -70,9 +52,7 @@ export const GrafanaPluginRootPage = (props: AppRootProps) => {
|
|||
};
|
||||
|
||||
export const Root = observer((props: AppRootProps) => {
|
||||
const { isBasicDataLoaded, loadBasicData, loadMasterData } = useStore();
|
||||
|
||||
const [pageTitle, setPageTitle] = useState('');
|
||||
const { isBasicDataLoaded, loadBasicData, loadMasterData, pageTitle } = useStore();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
|
|
@ -147,7 +127,7 @@ export const Root = observer((props: AppRootProps) => {
|
|||
<Incidents query={query} />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('alert-group')} exact>
|
||||
<Incident query={query} pageTitle={pageTitle} setPageTitle={setPageTitle} />
|
||||
<Incident query={query} />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('users')} exact>
|
||||
<Users query={query} />
|
||||
|
|
@ -165,7 +145,7 @@ export const Root = observer((props: AppRootProps) => {
|
|||
<Schedules query={query} />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('schedule')} exact>
|
||||
<Schedule query={query} pageTitle={pageTitle} setPageTitle={setPageTitle} />
|
||||
<Schedule query={query} />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('outgoing_webhooks')} exact>
|
||||
<OutgoingWebhooks query={query} />
|
||||
|
|
|
|||
20
grafana-plugin/src/plugin/dayjs.ts
Normal file
20
grafana-plugin/src/plugin/dayjs.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import isBetween from 'dayjs/plugin/isBetween';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
import localeData from 'dayjs/plugin/localeData';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import weekday from 'dayjs/plugin/weekday';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(weekday);
|
||||
dayjs.extend(localeData);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isoWeek);
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(customParseFormat);
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { locationService } from '@grafana/runtime';
|
||||
import { contextSrv } from 'grafana/app/core/core';
|
||||
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
|
||||
import moment from 'moment-timezone';
|
||||
import qs from 'query-string';
|
||||
import { OnCallAppPluginMeta } from 'types';
|
||||
|
||||
|
|
@ -28,7 +27,7 @@ import { ScheduleStore } from 'models/schedule/schedule';
|
|||
import { SlackStore } from 'models/slack/slack';
|
||||
import { SlackChannelStore } from 'models/slack_channel/slack_channel';
|
||||
import { TelegramChannelStore } from 'models/telegram_channel/telegram_channel';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { TimezoneStore } from 'models/timezone/timezone';
|
||||
import { UserStore } from 'models/user/user';
|
||||
import { UserGroupStore } from 'models/user_group/user_group';
|
||||
import { makeRequest } from 'network';
|
||||
|
|
@ -51,9 +50,6 @@ export class RootBaseStore {
|
|||
@observable
|
||||
isBasicDataLoaded = false;
|
||||
|
||||
@observable
|
||||
currentTimezone: Timezone = moment.tz.guess() as Timezone;
|
||||
|
||||
@observable
|
||||
backendVersion = '';
|
||||
|
||||
|
|
@ -83,6 +79,9 @@ export class RootBaseStore {
|
|||
@observable
|
||||
incidentFilters: any;
|
||||
|
||||
@observable
|
||||
pageTitle = '';
|
||||
|
||||
@observable
|
||||
incidentsPage: any = this.initialQuery.p ? Number(this.initialQuery.p) : 1;
|
||||
|
||||
|
|
@ -115,6 +114,7 @@ export class RootBaseStore {
|
|||
globalSettingStore = new GlobalSettingStore(this);
|
||||
filtersStore = new FiltersStore(this);
|
||||
labelsStore = new LabelStore(this);
|
||||
timezoneStore = new TimezoneStore(this);
|
||||
msteamsChannelStore: MSTeamsChannelStore = new MSTeamsChannelStore(this);
|
||||
loaderStore = LoaderStore;
|
||||
|
||||
|
|
@ -322,6 +322,11 @@ export class RootBaseStore {
|
|||
});
|
||||
}
|
||||
|
||||
@action.bound
|
||||
setPageTitle(title: string) {
|
||||
this.pageTitle = title;
|
||||
}
|
||||
|
||||
@action
|
||||
async removeSlackIntegration() {
|
||||
await this.slackStore.removeSlackIntegration();
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
"rootDirs": ["src"],
|
||||
"baseUrl": "src",
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"strict": false,
|
||||
"resolveJsonModule": true,
|
||||
"noImplicitAny": false,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue