Merge pull request #780 from grafana/666-display-ical-and-terraform-schedules-in-list

666 display ical and terraform schedules in list
This commit is contained in:
Yulia Shanyrova 2022-11-04 15:21:15 +01:00 committed by GitHub
commit f04e72e498
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 163 additions and 45 deletions

View file

@ -0,0 +1,31 @@
import React from 'react';
import { PENDING_COLOR, Tooltip, Icon } from '@grafana/ui';
import { Schedule } from 'models/schedule/schedule.types';
interface ScheduleWarningProps {
item: Schedule;
}
const ScheduleWarning = (props: ScheduleWarningProps) => {
const { item } = props;
if (item.warnings.length > 0) {
const tooltipContent = (
<div>
{item.warnings.map((warning: string, key: number) => (
<p key={key}>{warning}</p>
))}
</div>
);
return (
<Tooltip placement="top" content={tooltipContent}>
<Icon style={{ color: PENDING_COLOR }} name="exclamation-triangle" />
</Tooltip>
);
}
return null;
};
export default ScheduleWarning;

View file

@ -68,9 +68,8 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => {
</Field>
<Field label="Type">
<RadioButtonGroup
disabled
options={[
{ label: 'All', value: 'all' },
{ label: 'All', value: undefined },
{
label: 'Web',
value: ScheduleType.API,
@ -84,7 +83,7 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => {
value: ScheduleType.Calendar,
},
]}
value={value.type}
value={value?.type}
onChange={handleTypeChange}
/>
</Field>

View file

@ -18,7 +18,7 @@ import { Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event, Layer, S
export class ScheduleStore extends BaseStore {
@observable
searchResult: { [key: string]: Array<Schedule['id']> } = {};
searchResult: { results?: Array<Schedule['id']> } = {};
@observable.shallow
items: { [id: string]: Schedule } = {};
@ -105,8 +105,11 @@ export class ScheduleStore extends BaseStore {
}
@action
async updateItems(query = '') {
const result = await makeRequest(this.path, { method: 'GET', params: { search: query } });
async updateItems(f: any = { searchTerm: '', type: undefined }) {
// async updateItems(query = '') {
const filters = typeof f === 'string' ? { searchTerm: f } : f;
const { searchTerm: search, type } = filters;
const result = await makeRequest(this.path, { method: 'GET', params: { search: search, type } });
this.items = {
...this.items,
@ -118,10 +121,9 @@ export class ScheduleStore extends BaseStore {
{}
),
};
this.searchResult = {
...this.searchResult,
[query]: result.map((item: Schedule) => item.id),
results: result.map((item: Schedule) => item.id),
};
}
@ -136,12 +138,11 @@ export class ScheduleStore extends BaseStore {
}
}
getSearchResult(query = '') {
if (!this.searchResult[query]) {
getSearchResult() {
if (!this.searchResult.results) {
return undefined;
}
return this.searchResult[query].map((scheduleId: Schedule['id']) => this.items[scheduleId]);
return this.searchResult?.results?.map((scheduleId: Schedule['id']) => this.items[scheduleId]);
}
@action

View file

@ -2,12 +2,14 @@ import React from 'react';
import { AppRootProps } from '@grafana/data';
import { getLocationSrv } from '@grafana/runtime';
import { Button, HorizontalGroup, VerticalGroup, IconButton, ToolbarButton, Icon } from '@grafana/ui';
import { Button, HorizontalGroup, VerticalGroup, IconButton, ToolbarButton, Icon, Modal } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { omit } from 'lodash-es';
import { observer } from 'mobx-react';
import PluginLink from 'components/PluginLink/PluginLink';
import ScheduleWarning from 'components/ScheduleWarning/ScheduleWarning';
import Text from 'components/Text/Text';
import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect';
import WithConfirm from 'components/WithConfirm/WithConfirm';
@ -15,8 +17,9 @@ import Rotations from 'containers/Rotations/Rotations';
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 UsersTimezones from 'containers/UsersTimezones/UsersTimezones';
import { Shift } from 'models/schedule/schedule.types';
import { Schedule, ScheduleType, Shift } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
@ -24,7 +27,6 @@ import { withMobXProviderContext } from 'state/withStore';
import { getStartOfWeek } from './Schedule.helpers';
import styles from './Schedule.module.css';
const cx = cn.bind(styles);
interface SchedulePageProps extends AppRootProps, WithStoreProps {}
@ -37,6 +39,7 @@ interface SchedulePageState {
shiftIdToShowOverridesForm?: Shift['id'];
isLoading: boolean;
showEditForm: boolean;
showScheduleICalSettings: boolean;
}
@observer
@ -53,6 +56,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
shiftIdToShowOverridesForm: undefined,
isLoading: true,
showEditForm: false,
showScheduleICalSettings: false,
};
}
@ -89,6 +93,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
shiftIdToShowRotationForm,
shiftIdToShowOverridesForm,
showEditForm,
showScheduleICalSettings,
} = this.state;
const { scheduleStore, currentTimezone } = store;
@ -109,6 +114,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
<Text.Title editable editModalTitle="Schedule name" level={2} onTextChange={this.handleNameChange}>
{schedule?.name}
</Text.Title>
{schedule && <ScheduleWarning item={schedule} />}
</HorizontalGroup>
<HorizontalGroup spacing="lg">
{users && (
@ -118,6 +124,16 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
</HorizontalGroup>
)}
<HorizontalGroup>
{schedule?.type === ScheduleType.Ical && (
<HorizontalGroup>
<Button variant="secondary" onClick={this.handleExportClick()}>
Export
</Button>
<Button variant="secondary" onClick={this.handleReloadClick(scheduleId)}>
Reload
</Button>
</HorizontalGroup>
)}
<ToolbarButton
icon="cog"
tooltip="Settings"
@ -206,6 +222,16 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
}}
/>
)}
{showScheduleICalSettings && (
<Modal
isOpen
title="Schedule export"
closeOnEscape
onDismiss={() => this.setState({ showScheduleICalSettings: false })}
>
<ScheduleICalSettings id={scheduleId} />
</Modal>
)}
</>
);
}
@ -371,6 +397,47 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
this.setState({ startMoment: startMoment.add(7, 'day') }, this.handleDateRangeUpdate);
};
handleExportClick = () => {
return () => {
this.setState({ showScheduleICalSettings: true });
};
};
handleReloadClick = (scheduleId: Schedule['id']) => {
const { store } = this.props;
const { scheduleStore } = store;
return async () => {
await scheduleStore.reloadIcal(scheduleId);
scheduleStore.updateItem(scheduleId);
this.updateEventsFor(scheduleId);
};
};
updateEventsFor = async (scheduleId: Schedule['id'], withEmpty = true, with_gap = true) => {
const {
store,
query: { id },
} = this.props;
const { scheduleStore } = store;
store.scheduleStore.scheduleToScheduleEvents = omit(store.scheduleStore.scheduleToScheduleEvents, [scheduleId]);
await scheduleStore.updateScheduleEvents(
scheduleId,
withEmpty,
with_gap,
dayjs().format('YYYY-MM-DD').toString(),
dayjs.tz.guess()
);
await store.scheduleStore.updateOncallShifts(id);
await this.updateEvents();
};
handleDelete = () => {
const {
store,

View file

@ -11,6 +11,7 @@ import Avatar from 'components/Avatar/Avatar';
import NewScheduleSelector from 'components/NewScheduleSelector/NewScheduleSelector';
import PluginLink from 'components/PluginLink/PluginLink';
import ScheduleCounter from 'components/ScheduleCounter/ScheduleCounter';
import ScheduleWarning from 'components/ScheduleWarning/ScheduleWarning';
import SchedulesFilters from 'components/SchedulesFilters_NEW/SchedulesFilters';
import { SchedulesFiltersType } from 'components/SchedulesFilters_NEW/SchedulesFilters.types';
import Table from 'components/Table/Table';
@ -51,7 +52,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
const { store } = this.props;
this.state = {
startMoment: getStartOfWeek(store.currentTimezone),
filters: { searchTerm: '', status: 'all', type: ScheduleType.API },
filters: { searchTerm: '', status: 'all', type: undefined },
showNewScheduleSelector: false,
expandedRowKeys: [],
scheduleIdToEdit: undefined,
@ -80,10 +81,10 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
render: this.renderType,
},
{
width: '10%',
width: '5%',
title: 'Status',
key: 'name',
render: this.renderStatus,
render: (item: Schedule) => this.renderStatus(item),
},
{
width: '30%',
@ -107,6 +108,11 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
title: 'Slack user group',
render: this.renderUserGroup,
},
{
width: '5%',
key: 'warning',
render: this.renderWarning,
},
{
width: '50px',
key: 'buttons',
@ -119,7 +125,6 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
const data = schedules
? schedules
.filter((schedule) => schedule.type === ScheduleType.API)
.filter(
(schedule) =>
filters.status === 'all' ||
@ -265,38 +270,52 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
return typeToVerbal[value];
};
renderWarning = (item: Schedule) => {
return <ScheduleWarning item={item} />;
};
renderStatus = (item: Schedule) => {
const {
store: { scheduleStore },
} = this.props;
const relatedEscalationChains = scheduleStore.relatedEscalationChains[item.id];
return (
<HorizontalGroup>
<ScheduleCounter
type="link"
count={item.number_of_escalation_chains}
tooltipTitle="Used in escalations"
tooltipContent={
<VerticalGroup spacing="sm">
{relatedEscalationChains ? (
relatedEscalationChains.length ? (
relatedEscalationChains.map((escalationChain) => (
<PluginLink key={escalationChain.pk} query={{ page: 'escalations', id: escalationChain.pk }}>
{escalationChain.name}
</PluginLink>
))
{item.number_of_escalation_chains > 0 && (
<ScheduleCounter
type="link"
count={item.number_of_escalation_chains}
tooltipTitle="Used in escalations"
tooltipContent={
<VerticalGroup spacing="sm">
{relatedEscalationChains ? (
relatedEscalationChains.length ? (
relatedEscalationChains.map((escalationChain) => (
<div key={escalationChain.pk}>
<PluginLink query={{ page: 'escalations', id: escalationChain.pk }}>
{escalationChain.name}
</PluginLink>
</div>
))
) : (
'Not used yet'
)
) : (
'Not used yet'
)
) : (
<LoadingPlaceholder>Loading related escalation chains....</LoadingPlaceholder>
)}
</VerticalGroup>
}
onHover={this.getUpdateRelatedEscalationChainsHandler(item.id)}
/>
<LoadingPlaceholder>Loading related escalation chains....</LoadingPlaceholder>
)}
</VerticalGroup>
}
onHover={this.getUpdateRelatedEscalationChainsHandler(item.id)}
/>
)}
{/* <ScheduleCounter
type="warning"
count={warningsCount}
tooltipTitle="Warnings"
tooltipContent="Schedule has unassigned time periods during next 7 days"
/>*/}
</HorizontalGroup>
);
};
@ -372,9 +391,10 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
};
applyFilters = () => {
// const { filters } = this.state;
// const { scheduleStore } = this.props.store;
// scheduleStore.updateItems(filters.searchTerm);
const { filters } = this.state;
const { store } = this.props;
const { scheduleStore } = store;
scheduleStore.updateItems(filters);
};
debouncedUpdateSchedules = debounce(this.applyFilters, 1000);