From 23d706cc7f0afafc613aa44d422b2b5665263087 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 8 Sep 2022 12:42:16 +0300 Subject: [PATCH] add schedules filtering, add checing web schedules feature flag --- grafana-plugin/src/GrafanaPluginRootPage.tsx | 3 +- .../ScheduleCounter/ScheduleCounter.tsx | 5 +- .../SchedulesFilters_NEW/SchedulesFilters.tsx | 5 +- .../src/components/Table/Table.module.css | 4 + .../src/components/UserGroups/UserGroups.tsx | 12 +- .../containers/RotationForm/RotationForm.tsx | 2 +- .../RotationForm/ScheduleOverrideForm.tsx | 2 +- .../containers/Rotations/Rotations.module.css | 4 +- .../containers/ScheduleSlot/ScheduleSlot.tsx | 16 +-- .../escalation_chain.types.ts | 1 + .../src/models/schedule/schedule.ts | 21 +++- .../src/models/schedule/schedule.types.ts | 1 + grafana-plugin/src/pages/index.ts | 8 +- .../src/pages/schedule/Schedule.module.css | 4 + .../src/pages/schedule/Schedule.tsx | 9 +- .../pages/schedules_NEW/Schedules.module.css | 4 + .../src/pages/schedules_NEW/Schedules.tsx | 103 ++++++++++++++---- grafana-plugin/src/state/features.ts | 1 + grafana-plugin/src/utils/hooks.tsx | 22 +++- 19 files changed, 166 insertions(+), 61 deletions(-) diff --git a/grafana-plugin/src/GrafanaPluginRootPage.tsx b/grafana-plugin/src/GrafanaPluginRootPage.tsx index 3cd4d4a7..286bc509 100644 --- a/grafana-plugin/src/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/GrafanaPluginRootPage.tsx @@ -139,9 +139,10 @@ export const Root = observer((props: AppRootProps) => { grafanaUser: window.grafanaBootData.user, enableLiveSettings: store.hasFeature(AppFeature.LiveSettings), enableCloudPage: store.hasFeature(AppFeature.CloudConnection), + enableNewSchedulesPage: store.hasFeature(AppFeature.WebSchedules), backendLicense, }), - [meta, pathWithoutLeadingSlash, page, store.features] + [meta, pathWithoutLeadingSlash, page, store.features, backendLicense] ) ); useEffect(() => { diff --git a/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx b/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx index f436395b..d88013c6 100644 --- a/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx +++ b/grafana-plugin/src/components/ScheduleCounter/ScheduleCounter.tsx @@ -12,6 +12,7 @@ interface ScheduleCounterProps { count: number; tooltipTitle: string; tooltipContent: React.ReactNode; + onHover: () => void; } const typeToIcon = { @@ -37,7 +38,7 @@ const typeToBackgroundColor = { const cx = cn.bind(styles); const ScheduleCounter: FC = (props) => { - const { type, count, tooltipTitle, tooltipContent } = props; + const { type, count, tooltipTitle, tooltipContent, onHover } = props; return ( = (props) => { } > -
+
{count} diff --git a/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.tsx b/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.tsx index e893c2cf..4edc283f 100644 --- a/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.tsx +++ b/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.tsx @@ -69,11 +69,12 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => { { }, { label: 'API', - value: ScheduleType.API, + value: ScheduleType.Calendar, }, ]} value={value.type} diff --git a/grafana-plugin/src/components/Table/Table.module.css b/grafana-plugin/src/components/Table/Table.module.css index c2d7d959..df6caa08 100644 --- a/grafana-plugin/src/components/Table/Table.module.css +++ b/grafana-plugin/src/components/Table/Table.module.css @@ -17,6 +17,10 @@ background: rgba(63, 62, 62, 0.45); } +.root th:first-child { + padding-left: 20px; +} + .root td { min-height: 60px; padding: 10px 0; diff --git a/grafana-plugin/src/components/UserGroups/UserGroups.tsx b/grafana-plugin/src/components/UserGroups/UserGroups.tsx index 086aefc1..b726e6e4 100644 --- a/grafana-plugin/src/components/UserGroups/UserGroups.tsx +++ b/grafana-plugin/src/components/UserGroups/UserGroups.tsx @@ -64,7 +64,11 @@ const UserGroups = (props: UserGroupsProps) => { } const newGroups = [...value]; - const lastGroup = newGroups[newGroups.length - 1]; + let lastGroup = newGroups[newGroups.length - 1]; + if (!lastGroup) { + lastGroup = []; + newGroups.push(lastGroup); + } lastGroup.push(pk); @@ -73,11 +77,6 @@ const UserGroups = (props: UserGroupsProps) => { [value] ); - const filterUsers = useCallback( - ({ value: itemValue }) => !value.some((group: Array) => group.some((pk) => pk === itemValue)), - [value] - ); - const items = useMemo(() => toPlainArray(value, getItemData), [value]); const onSortEnd = useCallback( @@ -134,7 +133,6 @@ const UserGroups = (props: UserGroupsProps) => { value={null} onChange={handleUserAdd} getOptionLabel={({ label, value }: SelectableValue) => } - filterOptions={filterUsers} showError={showError} /> diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index bbb2aeda..e101e150 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx @@ -256,7 +256,7 @@ const RotationForm: FC = observer((props) => { [L{shiftId === 'new' ? layerPriority : shift?.priority_level}] - {shiftId === 'new' ? 'New Rotation' : shift?.id} + {shiftId === 'new' ? 'New Rotation' : 'Update Rotation'} diff --git a/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx b/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx index a34b117e..b73373f3 100644 --- a/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx @@ -198,7 +198,7 @@ const ScheduleOverrideForm: FC = (props) => { > - {shiftId === 'new' ? 'New Override' : shift?.id} + {shiftId === 'new' ? 'New Override' : 'Update Override'} diff --git a/grafana-plugin/src/containers/Rotations/Rotations.module.css b/grafana-plugin/src/containers/Rotations/Rotations.module.css index ce2935d5..e79d9f24 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.module.css +++ b/grafana-plugin/src/containers/Rotations/Rotations.module.css @@ -1,7 +1,7 @@ .root { - border: var(--border-medium); + border: var(--rotations-border); border-radius: 2px; - background: var(--primary-background); + background: var(--rotations-background); } .current-time { diff --git a/grafana-plugin/src/containers/ScheduleSlot/ScheduleSlot.tsx b/grafana-plugin/src/containers/ScheduleSlot/ScheduleSlot.tsx index bd233ab9..55043203 100644 --- a/grafana-plugin/src/containers/ScheduleSlot/ScheduleSlot.tsx +++ b/grafana-plugin/src/containers/ScheduleSlot/ScheduleSlot.tsx @@ -56,7 +56,14 @@ const ScheduleSlot: FC = observer((props) => { return (
- {event.is_empty ? ( + {event.is_gap ? ( + }> +
+ {trackMouse && mouseX > 0 &&
} + {label &&
{label}
} +
+ + ) : event.is_empty ? (
= observer((props) => {
)}
- ) : event.is_gap ? ( - }> -
- {trackMouse && mouseX > 0 &&
} - {label &&
{label}
} -
- ) : ( users.map(({ pk: userPk }, userIndex) => { const storeUser = store.userStore.items[userPk]; diff --git a/grafana-plugin/src/models/escalation_chain/escalation_chain.types.ts b/grafana-plugin/src/models/escalation_chain/escalation_chain.types.ts index 2ab202a8..55842020 100644 --- a/grafana-plugin/src/models/escalation_chain/escalation_chain.types.ts +++ b/grafana-plugin/src/models/escalation_chain/escalation_chain.types.ts @@ -1,5 +1,6 @@ export interface EscalationChain { id: string; + pk: string; //? because GET related_escalation_chains returns {name,pk}[] is_default: boolean; name: string; number_of_integrations: number; diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index 834ec036..41dc1b23 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -5,6 +5,7 @@ import { action, observable, toJS } from 'mobx'; import ReactCSSTransitionGroup from 'react-transition-group'; // ES6 import BaseStore from 'models/base_store'; +import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'; import { SlackChannel } from 'models/slack_channel/slack_channel.types'; import { Timezone } from 'models/timezone/timezone.types'; import { makeRequest } from 'network'; @@ -63,6 +64,9 @@ export class ScheduleStore extends BaseStore { @observable.shallow shifts: { [id: string]: Shift } = {}; + @observable.shallow + relatedEscalationChains: { [id: string]: EscalationChain[] } = {}; + @observable.shallow rotations: { [id: string]: { @@ -122,7 +126,7 @@ export class ScheduleStore extends BaseStore { @action async updateItems(query = '') { - const result = await this.getAll(); + const result = await makeRequest(this.path, { method: 'GET', params: { search: query } }); this.items = { ...this.items, @@ -259,6 +263,19 @@ export class ScheduleStore extends BaseStore { return response; } + updateRelatedEscalationChains = async (id: Schedule['id']) => { + const response = await makeRequest(`/schedules/${id}/related_escalation_chains`, { + method: 'GET', + }); + + this.relatedEscalationChains = { + ...this.relatedEscalationChains, + [id]: response, + }; + + return response; + }; + async updateRotationMock(rotationId: Rotation['id'], fromString: string, currentTimezone: Timezone) { if (this.rotations[rotationId]?.[fromString]) { return; @@ -342,7 +359,7 @@ export class ScheduleStore extends BaseStore { async deleteOncallShift(shiftId: Shift['id']) { return await makeRequest(`/oncall_shifts/${shiftId}`, { method: 'DELETE', - }); + }).catch(this.onApiError); } async updateEvents(scheduleId: Schedule['id'], startMoment: dayjs.Dayjs, type: RotationType = 'rotation', days = 9) { diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index 877cb8bc..c5a02e4a 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -25,6 +25,7 @@ export interface Schedule { mention_oncall_next: boolean; mention_oncall_start: boolean; notify_empty_oncall: number; + number_of_escalation_chains: number; } export interface ScheduleEvent { diff --git a/grafana-plugin/src/pages/index.ts b/grafana-plugin/src/pages/index.ts index 30f83e48..15561d51 100644 --- a/grafana-plugin/src/pages/index.ts +++ b/grafana-plugin/src/pages/index.ts @@ -64,14 +64,14 @@ export const pages: PageDefinition[] = [ { component: SchedulesPage2, icon: 'calendar-alt', - id: 'schedules-old', - text: 'Schedules OLD', + id: 'schedules', + text: 'Schedules', }, { component: SchedulesPage, icon: 'calendar-alt', - id: 'schedules', - text: 'Schedules', + id: 'schedules-new', + text: 'Schedules α', }, { component: SchedulePage, diff --git a/grafana-plugin/src/pages/schedule/Schedule.module.css b/grafana-plugin/src/pages/schedule/Schedule.module.css index 8c24c26a..2e5746bd 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.module.css +++ b/grafana-plugin/src/pages/schedule/Schedule.module.css @@ -1,7 +1,11 @@ + .root { max-width: 1600px; margin: 0 auto; margin-top: 24px; + + --rotations-border: var(--border-medium); + --rotations-background: var(--primary-background); } .header { diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 296e7a54..c82210d0 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -22,6 +22,7 @@ import ScheduleFinal from 'containers/Rotations/ScheduleFinal'; import ScheduleOverrides from 'containers/Rotations/ScheduleOverrides'; import { Timezone } from 'models/timezone/timezone.types'; import { User } from 'models/user/user.types'; +import { AppFeature } from 'state/features'; import { WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; @@ -58,6 +59,10 @@ class SchedulePage extends React.Component const { store } = this.props; const { startMoment } = this.state; + if (!store.hasFeature(AppFeature.WebSchedules)) { + getLocationSrv().update({ query: { page: 'schedules' } }); + } + store.userStore.updateItems(); const { @@ -89,7 +94,7 @@ class SchedulePage extends React.Component
- + @@ -334,7 +339,7 @@ class SchedulePage extends React.Component } = this.props; store.scheduleStore.delete(scheduleId).then(() => { - getLocationSrv().update({ query: { page: 'schedules' } }); + getLocationSrv().update({ query: { page: 'schedules-new' } }); }); }; diff --git a/grafana-plugin/src/pages/schedules_NEW/Schedules.module.css b/grafana-plugin/src/pages/schedules_NEW/Schedules.module.css index 19fd4532..4857e7cc 100644 --- a/grafana-plugin/src/pages/schedules_NEW/Schedules.module.css +++ b/grafana-plugin/src/pages/schedules_NEW/Schedules.module.css @@ -11,6 +11,10 @@ margin: 20px 0; } +.loader { + padding-left: 20px; +} + /* .root .expanded-row { background: var(--secondary-background); diff --git a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx index 25b2ec76..2bceb135 100644 --- a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { getLocationSrv } from '@grafana/runtime'; -import { Button, HorizontalGroup, IconButton, VerticalGroup } from '@grafana/ui'; +import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; import dayjs from 'dayjs'; +import { debounce } from 'lodash-es'; import { observer } from 'mobx-react'; import Avatar from 'components/Avatar/Avatar'; @@ -17,13 +18,12 @@ import Text from 'components/Text/Text'; import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect'; import WithConfirm from 'components/WithConfirm/WithConfirm'; -import Rotation from 'containers/Rotation/Rotation'; import ScheduleFinal from 'containers/Rotations/ScheduleFinal'; import { getFromString } from 'models/schedule/schedule.helpers'; import { Schedule, ScheduleType } from 'models/schedule/schedule.types'; -import { getTzOffsetString } from 'models/timezone/timezone.helpers'; import { Timezone } from 'models/timezone/timezone.types'; import { getStartOfWeek } from 'pages/schedule/Schedule.helpers'; +import { AppFeature } from 'state/features'; import { WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; @@ -48,7 +48,7 @@ class SchedulesPage extends React.Component schedule.type === ScheduleType.API) + .filter( + (schedule) => + filters.status === 'all' || + (filters.status === 'used' && schedule.number_of_escalation_chains) || + (filters.status === 'unused' && !schedule.number_of_escalation_chains) + ) + .filter((schedule) => !filters.searchTerm || schedule.name.includes(filters.searchTerm)) + : undefined; + return ( <>
@@ -132,7 +145,7 @@ class SchedulesPage extends React.Component cx('expanded-row'), }} + emptyText={ +
+ {data ? Not found : Loading schedules...} +
+ } /> @@ -186,7 +204,9 @@ class SchedulesPage extends React.Component { - store.scheduleStore.updateEvents(scheduleId, getFromString(startMoment), 'final'); + store.scheduleStore.updateEvents(scheduleId, startMoment, 'rotation'); + store.scheduleStore.updateEvents(scheduleId, startMoment, 'override'); + store.scheduleStore.updateEvents(scheduleId, startMoment, 'final'); }); }; @@ -218,25 +240,37 @@ class SchedulesPage extends React.Component { - const escalationsCount = Math.floor(Math.random() * 10) + 1; - const warningsCount = Math.floor(Math.random() * 10) + 1; + renderStatus = (item: Schedule) => { + const { + store: { scheduleStore }, + } = this.props; + + const relatedEscalationChains = scheduleStore.relatedEscalationChains[item.id]; return ( - Grafana 1 -
- Grafana 2 -
- Grafana 3 - + + {relatedEscalationChains ? ( + relatedEscalationChains.length ? ( + relatedEscalationChains.map((escalationChain) => ( + + {escalationChain.name} + + )) + ) : ( + 'Not used yet' + ) + ) : ( + Loading related escalation chains.... + )} + } + onHover={this.getUpdateRelatedEscalationChainsHandler(item.id)} /> {/* { - this.setState({ filters }); + this.setState({ filters }, this.debouncedUpdateSchedules); }; + applyFilters = () => { + const { filters } = this.state; + const { store } = this.props; + const { scheduleStore } = store; + + // scheduleStore.updateItems(filters.searchTerm); + }; + + debouncedUpdateSchedules = debounce(this.applyFilters, 1000); + handlePageChange = (page: number) => {}; update = () => { @@ -316,6 +360,17 @@ class SchedulesPage extends React.Component { + const { store } = this.props; + const { scheduleStore } = store; + + return () => { + scheduleStore.updateRelatedEscalationChains(scheduleId).then(() => { + this.forceUpdate(); + }); + }; + }; } export default withMobXProviderContext(SchedulesPage); diff --git a/grafana-plugin/src/state/features.ts b/grafana-plugin/src/state/features.ts index bf915f19..856d26d0 100644 --- a/grafana-plugin/src/state/features.ts +++ b/grafana-plugin/src/state/features.ts @@ -5,4 +5,5 @@ export enum AppFeature { MobileApp = 'mobile_app', CloudNotifications = 'grafana_cloud_notifications', CloudConnection = 'grafana_cloud_connection', + WebSchedules = 'web_schedules', } diff --git a/grafana-plugin/src/utils/hooks.tsx b/grafana-plugin/src/utils/hooks.tsx index 93052831..7c4adc74 100644 --- a/grafana-plugin/src/utils/hooks.tsx +++ b/grafana-plugin/src/utils/hooks.tsx @@ -1,5 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { useMemo } from 'react'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; import { AppRootProps, NavModelItem } from '@grafana/data'; @@ -18,11 +17,12 @@ type Args = { }; enableLiveSettings: boolean; enableCloudPage: boolean; + enableNewSchedulesPage: boolean; backendLicense: string; }; export function useForceUpdate() { - const [value, setValue] = useState(0); + const [, setValue] = useState(0); return () => setValue((value) => value + 1); } @@ -34,6 +34,7 @@ export function useNavModel({ grafanaUser, enableLiveSettings, enableCloudPage, + enableNewSchedulesPage, backendLicense, }: Args) { return useMemo(() => { @@ -49,7 +50,8 @@ export function useNavModel({ hideFromTabs || (role === 'Admin' && grafanaUser.orgRole !== role) || (id === 'live-settings' && !enableLiveSettings) || - (id === 'cloud' && !enableCloudPage), + (id === 'cloud' && !enableCloudPage) || + (id === 'schedules-new' && !enableNewSchedulesPage), }); if (page === id) { @@ -74,7 +76,17 @@ export function useNavModel({ node, main: node, }; - }, [meta.info.logos.large, pages, path, page, enableLiveSettings, enableCloudPage]); + }, [ + meta.info.logos.large, + pages, + path, + page, + enableLiveSettings, + enableCloudPage, + backendLicense, + enableNewSchedulesPage, + grafanaUser.orgRole, + ]); } export function usePrevious(value: any) {