Tweaks for integrations (#2869)
# What this PR does This PR adds a few tweaks to the integrations page - prevent calling `/counters` on the integration page, instead call it with `counters/[id]` - call to `/counters` will be done only once when we load all the integrations and prevent calling it again once we switch pages. Prior to this change every page resulted in a call to `/counters`, and due to the fact the request took on average `3-5s` it was being stalled by the browser on subsequent calls. - Call to `/connected_contact_points` is now conditioned by the integration type - No more page flicking when going back from integration page to the list of integrations - No more page flickering when switching pages in table view if there's more requests ongoing - Prevent discarding filters when going back from Integration view to the table view - Fixed routes not moving up/down - Fixed routes flickering when adding/removing a route These changes should result in slightly loader times because `/counters` was time-consuming request, and less UX issues. ## Which issue(s) this PR fixes - described at https://github.com/grafana/oncall/issues/2843 but incompletely ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
4cff4f2fa9
commit
26dee30cfa
25 changed files with 258 additions and 214 deletions
|
|
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- Performance and UX tweaks to integrations page ([#2869](https://github.com/grafana/oncall/pull/2869))
|
||||
|
||||
## v1.3.29 (2023-08-29)
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
|||
setDisabled(false);
|
||||
}, [prev, next]);
|
||||
|
||||
const onChangeItemsPerPageCallback = useCallback((option) => {
|
||||
const onChangeItemsPerPageCallback = useCallback((option: SelectableValue) => {
|
||||
setDisabled(true);
|
||||
onChangeItemsPerPage(option.value);
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@
|
|||
.integrationTree__group {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&--hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.integrationTree__icon {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import React, { useEffect, useState } from 'react';
|
|||
import { Icon, IconButton, IconName } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { isArray, isUndefined } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import styles from './IntegrationCollapsibleTreeView.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export interface IntegrationCollapsibleItem {
|
||||
isHidden?: boolean;
|
||||
customIcon?: IconName;
|
||||
canHoverIcon: boolean;
|
||||
collapsedView: (toggle?: () => void) => React.ReactNode; // needs toggle param for toggling on click
|
||||
|
|
@ -22,7 +24,7 @@ interface IntegrationCollapsibleTreeViewProps {
|
|||
configElements: Array<IntegrationCollapsibleItem | IntegrationCollapsibleItem[]>;
|
||||
}
|
||||
|
||||
const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewProps> = (props) => {
|
||||
const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewProps> = observer((props) => {
|
||||
const { configElements } = props;
|
||||
|
||||
const [expandedList, setExpandedList] = useState(getStartingExpandedState());
|
||||
|
|
@ -97,7 +99,7 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
|
|||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const IntegrationCollapsibleTreeItem: React.FC<{
|
||||
item: IntegrationCollapsibleItem;
|
||||
|
|
@ -107,7 +109,7 @@ const IntegrationCollapsibleTreeItem: React.FC<{
|
|||
const iconOnClickFn = !item.isCollapsible ? undefined : onClick;
|
||||
|
||||
return (
|
||||
<div className={cx('integrationTree__group')}>
|
||||
<div className={cx('integrationTree__group', { 'integrationTree__group--hidden': item.isHidden })}>
|
||||
<div className={cx('integrationTree__icon')}>
|
||||
{item.canHoverIcon ? (
|
||||
<IconButton name={getIconName()} onClick={iconOnClickFn} size="lg" />
|
||||
|
|
|
|||
|
|
@ -38,8 +38,7 @@ const IntegrationContactPoint: React.FC<{
|
|||
}> = observer(({ id }) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const contactPoints = alertReceiveChannelStore.connectedContactPoints[id];
|
||||
const warnings = contactPoints.filter((cp) => !cp.notificationConnected);
|
||||
|
||||
const warnings = contactPoints?.filter((cp) => !cp.notificationConnected);
|
||||
const [
|
||||
{
|
||||
isLoading,
|
||||
|
|
@ -88,6 +87,7 @@ const IntegrationContactPoint: React.FC<{
|
|||
<Drawer scrollableContent title="Connected Contact Points" onClose={closeDrawer} closeOnMaskClick={false}>
|
||||
<div className={cx('contactpoints__drawer')}>
|
||||
<GTable
|
||||
emptyText={'No contact points'}
|
||||
className={cx('contactpoints__table')}
|
||||
rowKey="id"
|
||||
data={contactPoints}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { ConfirmModal, HorizontalGroup, Icon, IconName } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
|
@ -15,7 +15,6 @@ import { ChannelFilter } from 'models/channel_filter';
|
|||
import CommonIntegrationHelper from 'pages/integration/CommonIntegration.helper';
|
||||
import IntegrationHelper from 'pages/integration/Integration.helper';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openNotification } from 'utils';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
|
|
@ -26,24 +25,39 @@ interface CollapsedIntegrationRouteDisplayProps {
|
|||
toggle: () => void;
|
||||
openEditTemplateModal: (templateName: string | string[], channelFilterId?: ChannelFilter['id']) => void;
|
||||
onEditRegexpTemplate: (channelFilterId: ChannelFilter['id']) => void;
|
||||
onRouteDelete: (routeId: string) => void;
|
||||
onItemMove: () => void;
|
||||
}
|
||||
|
||||
const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDisplayProps> = observer(
|
||||
({ channelFilterId, alertReceiveChannelId, routeIndex, toggle, openEditTemplateModal, onEditRegexpTemplate }) => {
|
||||
({
|
||||
channelFilterId,
|
||||
alertReceiveChannelId,
|
||||
routeIndex,
|
||||
toggle,
|
||||
openEditTemplateModal,
|
||||
onEditRegexpTemplate,
|
||||
onRouteDelete,
|
||||
onItemMove,
|
||||
}) => {
|
||||
const store = useStore();
|
||||
const { escalationChainStore, alertReceiveChannelStore } = store;
|
||||
const [routeIdForDeletion, setRouteIdForDeletion] = useState<ChannelFilter['id']>(undefined);
|
||||
|
||||
const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
|
||||
const routeWording = useMemo(() => {
|
||||
return CommonIntegrationHelper.getRouteConditionWording(
|
||||
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
|
||||
routeIndex
|
||||
);
|
||||
}, [routeIndex, alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId]]);
|
||||
|
||||
if (!channelFilter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const escalationChain = escalationChainStore.items[channelFilter.escalation_chain];
|
||||
const routeWording = CommonIntegrationHelper.getRouteConditionWording(
|
||||
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
|
||||
routeIndex
|
||||
);
|
||||
const chatOpsAvailableChannels = IntegrationHelper.getChatOpsChannels(channelFilter, store).filter(
|
||||
(channel) => channel
|
||||
);
|
||||
|
|
@ -59,10 +73,7 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
<div className={cx('heading-container__item', 'heading-container__item--large')}>
|
||||
<TooltipBadge
|
||||
borderType="success"
|
||||
text={CommonIntegrationHelper.getRouteConditionWording(
|
||||
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
|
||||
routeIndex
|
||||
)}
|
||||
text={routeWording}
|
||||
tooltipTitle={CommonIntegrationHelper.getRouteConditionTooltipWording(
|
||||
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
|
||||
routeIndex
|
||||
|
|
@ -93,6 +104,7 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
onItemMove={onItemMove}
|
||||
setRouteIdForDeletion={() => setRouteIdForDeletion(channelFilterId)}
|
||||
openRouteTemplateEditor={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
||||
/>
|
||||
|
|
@ -179,8 +191,7 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
|
|||
|
||||
async function onRouteDeleteConfirm() {
|
||||
setRouteIdForDeletion(undefined);
|
||||
await alertReceiveChannelStore.deleteChannelFilter(routeIdForDeletion);
|
||||
openNotification('Route has been deleted');
|
||||
onRouteDelete(routeIdForDeletion);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ interface ExpandedIntegrationRouteDisplayProps {
|
|||
templates: AlertTemplatesDTO[];
|
||||
openEditTemplateModal: (templateName: string | string[], channelFilterId?: ChannelFilter['id']) => void;
|
||||
onEditRegexpTemplate: (channelFilterId: ChannelFilter['id']) => void;
|
||||
onRouteDelete: (routeId: string) => void;
|
||||
onItemMove: () => void;
|
||||
}
|
||||
|
||||
interface ExpandedIntegrationRouteDisplayState {
|
||||
|
|
@ -59,7 +61,16 @@ interface ExpandedIntegrationRouteDisplayState {
|
|||
}
|
||||
|
||||
const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayProps> = observer(
|
||||
({ alertReceiveChannelId, channelFilterId, templates, routeIndex, openEditTemplateModal, onEditRegexpTemplate }) => {
|
||||
({
|
||||
alertReceiveChannelId,
|
||||
channelFilterId,
|
||||
templates,
|
||||
routeIndex,
|
||||
openEditTemplateModal,
|
||||
onEditRegexpTemplate,
|
||||
onRouteDelete,
|
||||
onItemMove,
|
||||
}) => {
|
||||
const store = useStore();
|
||||
const {
|
||||
telegramChannelStore,
|
||||
|
|
@ -130,6 +141,7 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
onItemMove={onItemMove}
|
||||
setRouteIdForDeletion={() => setState({ routeIdForDeletion: channelFilterId })}
|
||||
openRouteTemplateEditor={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
||||
/>
|
||||
|
|
@ -278,8 +290,7 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
|
||||
async function onRouteDeleteConfirm() {
|
||||
setState({ routeIdForDeletion: undefined });
|
||||
await alertReceiveChannelStore.deleteChannelFilter(routeIdForDeletion);
|
||||
openNotification('Route has been deleted');
|
||||
onRouteDelete(routeIdForDeletion);
|
||||
}
|
||||
|
||||
function onEscalationChainChange({ id }) {
|
||||
|
|
@ -319,6 +330,7 @@ interface RouteButtonsDisplayProps {
|
|||
routeIndex: number;
|
||||
setRouteIdForDeletion(): void;
|
||||
openRouteTemplateEditor(): void;
|
||||
onItemMove();
|
||||
}
|
||||
|
||||
export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
||||
|
|
@ -327,6 +339,7 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
|||
routeIndex,
|
||||
setRouteIdForDeletion,
|
||||
openRouteTemplateEditor,
|
||||
onItemMove,
|
||||
}) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
|
|
@ -404,11 +417,13 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
|||
function onRouteMoveDown(e: React.SyntheticEvent) {
|
||||
e.stopPropagation();
|
||||
alertReceiveChannelStore.moveChannelFilterToPosition(alertReceiveChannelId, routeIndex, routeIndex + 1);
|
||||
onItemMove();
|
||||
}
|
||||
|
||||
function onRouteMoveUp(e: React.SyntheticEvent) {
|
||||
e.stopPropagation();
|
||||
alertReceiveChannelStore.moveChannelFilterToPosition(alertReceiveChannelId, routeIndex, routeIndex - 1);
|
||||
onItemMove();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
|
|||
import { SelectOption, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { PAGE } from 'utils/consts';
|
||||
|
||||
import { parseFilters } from './RemoteFilters.helpers';
|
||||
import { FilterOption, RemoteFiltersType } from './RemoteFilters.types';
|
||||
|
|
@ -39,7 +40,7 @@ interface RemoteFiltersProps extends WithStoreProps {
|
|||
value: RemoteFiltersType;
|
||||
onChange: (filters: { [key: string]: any }, isOnMount: boolean) => void;
|
||||
query: { [key: string]: any };
|
||||
page: string;
|
||||
page: PAGE;
|
||||
defaultFilters?: FiltersValues;
|
||||
extraFilters?: (state, setState, onFiltersValueChange) => React.ReactNode;
|
||||
grafanaTeamStore: GrafanaTeamStore;
|
||||
|
|
@ -378,6 +379,7 @@ class RemoteFilters extends Component<RemoteFiltersProps, RemoteFiltersState> {
|
|||
}
|
||||
|
||||
LocationHelper.update({ ...values }, 'partial');
|
||||
|
||||
onChange(values, isOnMount);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -131,10 +131,14 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return results;
|
||||
}
|
||||
|
||||
async updatePaginatedItems(query: any = '', page = 1) {
|
||||
async updatePaginatedItems(query: any = '', page = 1, updateCounters = false, invalidateFn = undefined) {
|
||||
const filters = typeof query === 'string' ? { search: query } : query;
|
||||
const { count, results } = await makeRequest(this.path, { params: { ...filters, page } });
|
||||
|
||||
if (invalidateFn?.()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.items = {
|
||||
...this.items,
|
||||
...results.reduce(
|
||||
|
|
@ -153,7 +157,9 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
results: results.map((item: AlertReceiveChannel) => item.id),
|
||||
};
|
||||
|
||||
this.updateCounters();
|
||||
if (updateCounters) {
|
||||
this.updateCounters();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
|
@ -297,7 +303,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
method: 'DELETE',
|
||||
});
|
||||
|
||||
this.updateChannelFilters(channelFilter.alert_receive_channel, true);
|
||||
return this.updateChannelFilters(channelFilter.alert_receive_channel, true);
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -499,6 +505,21 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
this.counters = counters;
|
||||
}
|
||||
|
||||
async updateCountersForIntegration(id: AlertReceiveChannel['id']): Promise<any> {
|
||||
const counters = await makeRequest(`${this.path}${id}/counters`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
this.counters = {
|
||||
...this.counters,
|
||||
[id]: {
|
||||
...counters[id],
|
||||
},
|
||||
};
|
||||
|
||||
return counters;
|
||||
}
|
||||
|
||||
startMaintenanceMode = (id: AlertReceiveChannel['id'], mode: MaintenanceMode, duration: number): Promise<void> =>
|
||||
makeRequest<null>(`${this.path}${id}/start_maintenance/`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -111,35 +111,35 @@ export class UserStore extends BaseStore {
|
|||
}
|
||||
|
||||
@action
|
||||
async updateItems(f: any = { searchTerm: '' }, page = 1) {
|
||||
return new Promise<void>(async (resolve) => {
|
||||
const filters = typeof f === 'string' ? { searchTerm: f } : f; // for GSelect compatibility
|
||||
const { searchTerm: search } = filters;
|
||||
const { count, results } = await makeRequest(this.path, {
|
||||
params: { search, page },
|
||||
});
|
||||
|
||||
this.items = {
|
||||
...this.items,
|
||||
...results.reduce(
|
||||
(acc: { [key: number]: User }, item: User) => ({
|
||||
...acc,
|
||||
[item.pk]: {
|
||||
...item,
|
||||
timezone: getTimezone(item),
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
};
|
||||
|
||||
this.searchResult = {
|
||||
count,
|
||||
results: results.map((item: User) => item.pk),
|
||||
};
|
||||
|
||||
resolve();
|
||||
async updateItems(f: any = { searchTerm: '' }, page = 1): Promise<any> {
|
||||
const filters = typeof f === 'string' ? { searchTerm: f } : f; // for GSelect compatibility
|
||||
const { searchTerm: search } = filters;
|
||||
const response = await makeRequest(this.path, {
|
||||
params: { search, page },
|
||||
});
|
||||
|
||||
const { count, results } = response;
|
||||
|
||||
this.items = {
|
||||
...this.items,
|
||||
...results.reduce(
|
||||
(acc: { [key: number]: User }, item: User) => ({
|
||||
...acc,
|
||||
[item.pk]: {
|
||||
...item,
|
||||
timezone: getTimezone(item),
|
||||
},
|
||||
}),
|
||||
{}
|
||||
),
|
||||
};
|
||||
|
||||
this.searchResult = {
|
||||
count,
|
||||
results: results.map((item: User) => item.pk),
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
getSearchResult() {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { FiltersValues } from 'models/filters/filters.types';
|
|||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
import { PAGE, PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import styles from './EscalationChains.module.css';
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
|
|||
<div className={cx('filters')}>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page="escalation_chains"
|
||||
page={PAGE.Escalations}
|
||||
grafanaTeamStore={store.grafanaTeamStore}
|
||||
onChange={this.handleFiltersChange}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { PageProps, WithStoreProps } from 'state/types';
|
|||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
import { PAGE, PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import styles from './Incidents.module.scss';
|
||||
import { IncidentDropdown } from './parts/IncidentDropdown';
|
||||
|
|
@ -264,7 +264,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
<div className={cx('filters')}>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page="incidents"
|
||||
page={PAGE.Incidents}
|
||||
onChange={this.handleFiltersChange}
|
||||
extraFilters={this.renderCards.bind(this)}
|
||||
grafanaTeamStore={store.grafanaTeamStore}
|
||||
|
|
@ -434,12 +434,15 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
|
||||
renderTable() {
|
||||
const { selectedIncidentIds, pagination } = this.state;
|
||||
const { store } = this.props;
|
||||
const { alertGroupsLoading } = store.alertGroupStore;
|
||||
const {
|
||||
store,
|
||||
store: { alertGroupStore, filtersStore },
|
||||
} = this.props;
|
||||
|
||||
const results = store.alertGroupStore.getAlertSearchResult('default');
|
||||
const prev = get(store.alertGroupStore.alertsSearchResult, `default.prev`);
|
||||
const next = get(store.alertGroupStore.alertsSearchResult, `default.next`);
|
||||
const results = alertGroupStore.getAlertSearchResult('default');
|
||||
const prev = get(alertGroupStore.alertsSearchResult, `default.prev`);
|
||||
const next = get(alertGroupStore.alertsSearchResult, `default.next`);
|
||||
const isLoading = alertGroupStore.alertGroupsLoading || filtersStore.options['incidents'] === undefined;
|
||||
|
||||
if (results && !results.length) {
|
||||
return (
|
||||
|
|
@ -517,8 +520,8 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
<div className={cx('root')}>
|
||||
{this.renderBulkActions()}
|
||||
<GTable
|
||||
emptyText={alertGroupsLoading ? 'Loading...' : 'No alert groups found'}
|
||||
loading={alertGroupsLoading}
|
||||
emptyText={isLoading ? 'Loading...' : 'No alert groups found'}
|
||||
loading={isLoading}
|
||||
className={cx('incidents-table')}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedIncidentIds,
|
||||
|
|
@ -531,7 +534,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
<div className={cx('pagination')}>
|
||||
<CursorPagination
|
||||
current={`${pagination.start}-${pagination.end}`}
|
||||
itemsPerPage={store.alertGroupStore.incidentsItemsPerPage}
|
||||
itemsPerPage={alertGroupStore.incidentsItemsPerPage}
|
||||
itemsPerPageOptions={[
|
||||
{ label: '25', value: 25 },
|
||||
{ label: '50', value: 50 },
|
||||
|
|
|
|||
|
|
@ -97,14 +97,6 @@ export const pages: { [id: string]: PageDefinition } = [
|
|||
hideFromTabs: isTopNavbar(),
|
||||
action: UserActions.ChatOpsRead,
|
||||
},
|
||||
{
|
||||
icon: 'wrench',
|
||||
id: 'maintenance',
|
||||
text: 'Maintenance',
|
||||
hideFromBreadcrumbs: true,
|
||||
path: getPath('maintenance'),
|
||||
action: UserActions.MaintenanceRead,
|
||||
},
|
||||
{
|
||||
icon: 'cog',
|
||||
id: 'settings',
|
||||
|
|
@ -169,7 +161,6 @@ export const ROUTES = {
|
|||
schedules: ['schedules'],
|
||||
schedule: ['schedules/:id'],
|
||||
outgoing_webhooks: ['outgoing_webhooks', 'outgoing_webhooks/:id', 'outgoing_webhooks/:action/:id'],
|
||||
maintenance: ['maintenance'],
|
||||
settings: ['settings'],
|
||||
'chat-ops': ['chat-ops'],
|
||||
'live-settings': ['live-settings'],
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ import { MAX_CHARACTERS_COUNT, TEXTAREA_ROWS_COUNT } from './IntegrationCommon.c
|
|||
|
||||
const IntegrationHelper = {
|
||||
isGrafanaAlerting: (alertReceiveChannel: AlertReceiveChannel | string) => {
|
||||
if (!alertReceiveChannel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof alertReceiveChannel === 'string') {
|
||||
return alertReceiveChannel === 'grafana_alerting';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
Alert,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { get, noop } from 'lodash-es';
|
||||
import { get } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import Emoji from 'react-emoji-render';
|
||||
|
|
@ -112,7 +112,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
this.openEditTemplateModal(query.template, query.routeId && query.routeId);
|
||||
}
|
||||
|
||||
await this.loadIntegration();
|
||||
await this.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
@ -127,7 +127,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
} = this.state;
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
query: { p },
|
||||
query,
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
|
|
@ -181,7 +181,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
)}
|
||||
|
||||
<div className={cx('integration__heading-container')}>
|
||||
<PluginLink query={{ page: 'integrations', p }} className={cx('back-arrow')}>
|
||||
<PluginLink query={{ page: 'integrations', ...query }} className={cx('back-arrow')}>
|
||||
<IconButton name="arrow-left" size="xl" />
|
||||
</PluginLink>
|
||||
<h2 className={cx('integration__name')}>
|
||||
|
|
@ -200,7 +200,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
{this.renderDescriptionMaybe(alertReceiveChannel)}
|
||||
|
||||
{/* MobX seems to have issues updating contact points if we don't reference it here */}
|
||||
{!contactPoints?.length && this.renderContactPointsWarningMaybe(alertReceiveChannel)}
|
||||
{contactPoints && contactPoints.length === 0 && this.renderContactPointsWarningMaybe(alertReceiveChannel)}
|
||||
|
||||
<div className={cx('no-wrap')}>
|
||||
<IntegrationHeader
|
||||
|
|
@ -286,7 +286,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
To ensure a smooth transition you can migrate now using "Migrate" button in the menu on the right.
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
Please, check{' '}
|
||||
Please check out the{' '}
|
||||
<a
|
||||
href={`https://grafana.com/docs/oncall/latest/integrations/${getIntegrationName()}`}
|
||||
target="_blank"
|
||||
|
|
@ -356,9 +356,11 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
} = this.props;
|
||||
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[id];
|
||||
const contactPoints = alertReceiveChannelStore.connectedContactPoints[id];
|
||||
|
||||
return [
|
||||
IntegrationHelper.isGrafanaAlerting(alertReceiveChannel) && {
|
||||
isHidden: contactPoints === null || contactPoints === undefined,
|
||||
isCollapsible: false,
|
||||
customIcon: 'grafana',
|
||||
canHoverIcon: false,
|
||||
|
|
@ -482,7 +484,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
};
|
||||
|
||||
handleAddNewRoute = () => {
|
||||
const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store;
|
||||
const { alertReceiveChannelStore } = this.props.store;
|
||||
const {
|
||||
params: { id },
|
||||
} = this.props.match;
|
||||
|
|
@ -499,12 +501,16 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
filtering_term_type: 1, // non-regex
|
||||
})
|
||||
.then(async (channelFilter: ChannelFilter) => {
|
||||
this.setState((prevState) => ({
|
||||
isAddingRoute: false,
|
||||
openRoutes: prevState.openRoutes.concat(channelFilter.id),
|
||||
}));
|
||||
await alertReceiveChannelStore.updateChannelFilters(id, true);
|
||||
await escalationPolicyStore.updateEscalationPolicies(channelFilter.escalation_chain);
|
||||
await alertReceiveChannelStore.updateChannelFilters(id);
|
||||
|
||||
this.setState(
|
||||
(prevState) => ({
|
||||
isAddingRoute: false,
|
||||
openRoutes: prevState.openRoutes.concat(channelFilter.id),
|
||||
}),
|
||||
() => this.forceUpdate()
|
||||
);
|
||||
|
||||
openNotification('A new route has been added');
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
@ -530,7 +536,12 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
const templates = alertReceiveChannelStore.templates[id];
|
||||
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[id];
|
||||
|
||||
return channelFilterIds.map(
|
||||
const onRouteDelete = async (routeId: string) => {
|
||||
await alertReceiveChannelStore.deleteChannelFilter(routeId).then(() => this.forceUpdate());
|
||||
openNotification('Route has been deleted');
|
||||
};
|
||||
|
||||
return (channelFilterIds || []).map(
|
||||
(channelFilterId: ChannelFilter['id'], routeIndex: number) =>
|
||||
({
|
||||
canHoverIcon: true,
|
||||
|
|
@ -550,8 +561,10 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
toggle={toggle}
|
||||
onItemMove={() => this.forceUpdate()}
|
||||
openEditTemplateModal={this.openEditTemplateModal}
|
||||
onEditRegexpTemplate={this.handleEditRegexpRouteTemplate}
|
||||
onRouteDelete={onRouteDelete}
|
||||
/>
|
||||
),
|
||||
expandedView: () => (
|
||||
|
|
@ -562,6 +575,8 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
templates={templates}
|
||||
openEditTemplateModal={this.openEditTemplateModal}
|
||||
onEditRegexpTemplate={this.handleEditRegexpRouteTemplate}
|
||||
onItemMove={() => this.forceUpdate()}
|
||||
onRouteDelete={onRouteDelete}
|
||||
/>
|
||||
),
|
||||
} as IntegrationCollapsibleItem)
|
||||
|
|
@ -655,7 +670,7 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
alertReceiveChannelStore.deleteAlertReceiveChannel(id).then(() => history.push(`${PLUGIN_ROOT}/integrations/`));
|
||||
};
|
||||
|
||||
async loadIntegration() {
|
||||
async loadData() {
|
||||
const {
|
||||
store,
|
||||
store: { alertReceiveChannelStore },
|
||||
|
|
@ -668,12 +683,9 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
const promises = [];
|
||||
|
||||
if (!alertReceiveChannelStore.items[id]) {
|
||||
// See what happens if the request fails
|
||||
promises.push(alertReceiveChannelStore.loadItem(id));
|
||||
}
|
||||
|
||||
if (!alertReceiveChannelStore.counters[id]) {
|
||||
promises.push(alertReceiveChannelStore.updateCounters());
|
||||
promises.push(alertReceiveChannelStore.loadItem(id).then(() => this.loadExtraData(id)));
|
||||
} else {
|
||||
promises.push(this.loadExtraData(id));
|
||||
}
|
||||
|
||||
if (!alertReceiveChannelStore.channelFilterIds[id]) {
|
||||
|
|
@ -681,12 +693,8 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
}
|
||||
|
||||
promises.push(alertReceiveChannelStore.updateTemplates(id));
|
||||
|
||||
promises.push(IntegrationHelper.fetchChatOps(store));
|
||||
|
||||
// skip checking for grafana alerting so that we don't wait for the first request to complete
|
||||
// at the cost of getting a failed network request for all other types other than alerting
|
||||
promises.push(alertReceiveChannelStore.updateConnectedContactPoints(id).catch(noop));
|
||||
promises.push(alertReceiveChannelStore.updateCountersForIntegration(id));
|
||||
|
||||
await Promise.all(promises)
|
||||
.catch(() => {
|
||||
|
|
@ -697,6 +705,17 @@ class Integration extends React.Component<IntegrationProps, IntegrationState> {
|
|||
})
|
||||
.finally(() => this.setState({ isLoading: false }));
|
||||
}
|
||||
|
||||
async loadExtraData(id: AlertReceiveChannel['id']) {
|
||||
const { alertReceiveChannelStore } = this.props.store;
|
||||
|
||||
if (IntegrationHelper.isGrafanaAlerting(alertReceiveChannelStore.items[id])) {
|
||||
// this will be delayed and not awaitable so that we don't delay the whole page load
|
||||
return await alertReceiveChannelStore.updateConnectedContactPoints(id);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
interface IntegrationActionsProps {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { withMobXProviderContext } from 'state/withStore';
|
|||
import { openNotification } from 'utils';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { PAGE } from 'utils/consts';
|
||||
|
||||
import styles from './Integrations.module.scss';
|
||||
|
||||
|
|
@ -45,7 +46,6 @@ const MAX_LINE_LENGTH = 40;
|
|||
interface IntegrationsState extends PageBaseState {
|
||||
integrationsFilters: Filters;
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'] | 'new';
|
||||
page: number;
|
||||
confirmationModal: {
|
||||
isOpen: boolean;
|
||||
title: any;
|
||||
|
|
@ -62,20 +62,21 @@ interface IntegrationsProps extends WithStoreProps, PageProps, RouteComponentPro
|
|||
|
||||
@observer
|
||||
class Integrations extends React.Component<IntegrationsProps, IntegrationsState> {
|
||||
state: IntegrationsState = {
|
||||
integrationsFilters: { searchTerm: '' },
|
||||
errorData: initErrorDataState(),
|
||||
page: 1,
|
||||
confirmationModal: undefined,
|
||||
};
|
||||
constructor(props: IntegrationsProps) {
|
||||
super(props);
|
||||
|
||||
const { query, store } = props;
|
||||
|
||||
this.state = {
|
||||
integrationsFilters: { searchTerm: '' },
|
||||
errorData: initErrorDataState(),
|
||||
confirmationModal: undefined,
|
||||
};
|
||||
|
||||
store.currentPage['integrations'] = Number(store.currentPage['integrations'] || query.p || 1);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
query: { p },
|
||||
} = this.props;
|
||||
|
||||
this.setState({ page: p ? Number(p) : 1 }, this.update);
|
||||
|
||||
this.parseQueryParams();
|
||||
}
|
||||
|
||||
|
|
@ -118,15 +119,19 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
|
||||
update = () => {
|
||||
const { store } = this.props;
|
||||
const { page, integrationsFilters } = this.state;
|
||||
const { integrationsFilters } = this.state;
|
||||
const page = store.currentPage['integrations'];
|
||||
|
||||
LocationHelper.update({ p: page }, 'partial');
|
||||
|
||||
return store.alertReceiveChannelStore.updatePaginatedItems(integrationsFilters, page);
|
||||
return store.alertReceiveChannelStore.updatePaginatedItems(integrationsFilters, page, false, () =>
|
||||
this.invalidateRequestFn(page)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { store, query } = this.props;
|
||||
const { alertReceiveChannelId, page, confirmationModal } = this.state;
|
||||
const { alertReceiveChannelId, confirmationModal } = this.state;
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
const { count, results } = alertReceiveChannelStore.getPaginatedSearchResult();
|
||||
|
|
@ -158,12 +163,13 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<div>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page="integrations"
|
||||
page={PAGE.Integrations}
|
||||
grafanaTeamStore={store.grafanaTeamStore}
|
||||
onChange={this.handleIntegrationsFiltersChange}
|
||||
/>
|
||||
<GTable
|
||||
emptyText={this.renderNotFound()}
|
||||
emptyText={count === undefined ? 'Loading...' : 'No integrations found'}
|
||||
loading={count === undefined}
|
||||
data-testid="integrations-table"
|
||||
rowKey="id"
|
||||
data={results}
|
||||
|
|
@ -171,13 +177,14 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
className={cx('integrations-table')}
|
||||
rowClassName={cx('integrations-table-row')}
|
||||
pagination={{
|
||||
page,
|
||||
page: store.currentPage['integrations'],
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{alertReceiveChannelId && (
|
||||
<IntegrationForm
|
||||
onHide={() => {
|
||||
|
|
@ -209,21 +216,17 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
);
|
||||
}
|
||||
|
||||
renderNotFound() {
|
||||
return (
|
||||
<div className={cx('loader')}>
|
||||
<Text type="secondary">Not found</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderName = (item: AlertReceiveChannel) => {
|
||||
const {
|
||||
query: { p },
|
||||
} = this.props;
|
||||
const { query } = this.props;
|
||||
|
||||
return (
|
||||
<PluginLink query={{ page: 'integrations', id: item.id, p }}>
|
||||
<PluginLink
|
||||
query={{
|
||||
page: 'integrations',
|
||||
id: item.id,
|
||||
...query,
|
||||
}}
|
||||
>
|
||||
<Text type="link" size="medium">
|
||||
<Emoji
|
||||
className={cx('title')}
|
||||
|
|
@ -469,8 +472,16 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
];
|
||||
};
|
||||
|
||||
invalidateRequestFn = (requestedPage: number) => {
|
||||
const { store } = this.props;
|
||||
return requestedPage !== store.getCurrentPage(PAGE.Integrations);
|
||||
};
|
||||
|
||||
handleChangePage = (page: number) => {
|
||||
this.setState({ page }, this.update);
|
||||
const { store } = this.props;
|
||||
|
||||
store.currentPage['integrations'] = page;
|
||||
this.update();
|
||||
};
|
||||
|
||||
onIntegrationEditClick = (id: AlertReceiveChannel['id']) => {
|
||||
|
|
@ -486,19 +497,23 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
this.setState({ confirmationModal: undefined });
|
||||
};
|
||||
|
||||
handleIntegrationsFiltersChange = (integrationsFilters: Filters) => {
|
||||
this.setState({ integrationsFilters }, () => this.debouncedUpdateIntegrations());
|
||||
handleIntegrationsFiltersChange = (integrationsFilters: Filters, isOnMount: boolean) => {
|
||||
this.setState({ integrationsFilters }, () => this.debouncedUpdateIntegrations(isOnMount));
|
||||
};
|
||||
|
||||
applyFilters = () => {
|
||||
applyFilters = async (isOnMount: boolean) => {
|
||||
const { store } = this.props;
|
||||
const { alertReceiveChannelStore } = store;
|
||||
const { integrationsFilters } = this.state;
|
||||
|
||||
return alertReceiveChannelStore.updatePaginatedItems(integrationsFilters).then(() => {
|
||||
this.setState({ page: 1 });
|
||||
LocationHelper.update({ p: 1 }, 'partial');
|
||||
});
|
||||
const newPage = isOnMount ? store.getCurrentPage(PAGE.Integrations) : 1;
|
||||
|
||||
return alertReceiveChannelStore
|
||||
.updatePaginatedItems(integrationsFilters, newPage, false, () => this.invalidateRequestFn(newPage))
|
||||
.then(() => {
|
||||
store.setCurrentPage(PAGE.Integrations, newPage);
|
||||
LocationHelper.update({ p: newPage }, 'partial');
|
||||
});
|
||||
};
|
||||
|
||||
debouncedUpdateIntegrations = debounce(this.applyFilters, FILTERS_DEBOUNCE_MS);
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
.select {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: var(--title-marginBottom);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
|
||||
import styles from './Maintenance.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface MaintenancePageProps {}
|
||||
|
||||
@observer
|
||||
class MaintenancePage extends React.Component<MaintenancePageProps> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Alert
|
||||
severity="info"
|
||||
className={cx('info-box')}
|
||||
// @ts-ignore
|
||||
title={
|
||||
<>
|
||||
Maintenance mode is now controlled at the{' '}
|
||||
<PluginLink query={{ page: 'integrations' }}> Integration</PluginLink> level. This page will soon be
|
||||
removed.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MaintenancePage;
|
||||
|
|
@ -36,7 +36,7 @@ import { PageProps, WithStoreProps } from 'state/types';
|
|||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { openErrorNotification, openNotification } from 'utils';
|
||||
import { isUserActionAllowed, UserActions } from 'utils/authorization';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
import { PAGE, PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import styles from './OutgoingWebhooks.module.scss';
|
||||
import { WebhookFormActionType } from './OutgoingWebhooks.types';
|
||||
|
|
@ -225,7 +225,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
<div className={cx('filters')}>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page="webhooks"
|
||||
page={PAGE.Webhooks}
|
||||
grafanaTeamStore={store.grafanaTeamStore}
|
||||
onChange={this.handleFiltersChange}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import EscalationsChainsPage from 'pages/escalation-chains/EscalationChains';
|
||||
import IncidentPage from 'pages/incident/Incident';
|
||||
import IncidentsPage from 'pages/incidents/Incidents';
|
||||
import MaintenancePage from 'pages/maintenance/Maintenance';
|
||||
import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks';
|
||||
import SchedulePage from 'pages/schedule/Schedule';
|
||||
import SchedulesPage from 'pages/schedules/Schedules';
|
||||
|
|
@ -55,10 +54,6 @@ export const routes: { [id: string]: NavRoute } = [
|
|||
component: OutgoingWebhooks,
|
||||
id: 'outgoing_webhooks',
|
||||
},
|
||||
{
|
||||
component: MaintenancePage,
|
||||
id: 'maintenance',
|
||||
},
|
||||
{
|
||||
component: SettingsPage,
|
||||
id: 'settings',
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { WithStoreProps, PageProps } from 'state/types';
|
|||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { PLUGIN_ROOT, TABLE_COLUMN_MAX_WIDTH } from 'utils/consts';
|
||||
import { PAGE, PLUGIN_ROOT, TABLE_COLUMN_MAX_WIDTH } from 'utils/consts';
|
||||
|
||||
import styles from './Schedules.module.css';
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
<div className={cx('schedules__filters-container')}>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page="schedules"
|
||||
page={PAGE.Schedules}
|
||||
grafanaTeamStore={store.grafanaTeamStore}
|
||||
onChange={this.handleSchedulesFiltersChange}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -86,14 +86,6 @@
|
|||
"action": "grafana-oncall-app.outgoing-webhooks:read",
|
||||
"addToNav": true
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"name": "Maintenance",
|
||||
"path": "/a/grafana-oncall-app/maintenance",
|
||||
"role": "Viewer",
|
||||
"action": "grafana-oncall-app.maintenance:read",
|
||||
"addToNav": true
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"name": "Settings",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import Incident from 'pages/incident/Incident';
|
|||
import Incidents from 'pages/incidents/Incidents';
|
||||
import Integration from 'pages/integration/Integration';
|
||||
import Integrations from 'pages/integrations/Integrations';
|
||||
import Maintenance from 'pages/maintenance/Maintenance';
|
||||
import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks';
|
||||
import Schedule from 'pages/schedule/Schedule';
|
||||
import Schedules from 'pages/schedules/Schedules';
|
||||
|
|
@ -157,9 +156,6 @@ export const Root = observer((props: AppRootProps) => {
|
|||
<Route path={getRoutesForPage('outgoing_webhooks')} exact>
|
||||
<OutgoingWebhooks query={query} />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('maintenance')} exact>
|
||||
<Maintenance />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('settings')} exact>
|
||||
<SettingsPage />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
CLOUD_VERSION_REGEX,
|
||||
GRAFANA_LICENSE_CLOUD,
|
||||
GRAFANA_LICENSE_OSS,
|
||||
PAGE,
|
||||
PLUGIN_ROOT,
|
||||
} from 'utils/consts';
|
||||
import FaroHelper from 'utils/faro';
|
||||
|
|
@ -79,6 +80,9 @@ export class RootBaseStore {
|
|||
@observable
|
||||
incidentsPage: any = this.initialQuery.p ? Number(this.initialQuery.p) : 1;
|
||||
|
||||
@observable
|
||||
currentPage: { [key: string]: number } = {};
|
||||
|
||||
@observable
|
||||
onCallApiUrl: string;
|
||||
|
||||
|
|
@ -297,4 +301,13 @@ export class RootBaseStore {
|
|||
const settings = await PluginState.getGrafanaPluginSettings();
|
||||
return settings.jsonData?.onCallApiUrl;
|
||||
}
|
||||
|
||||
getCurrentPage = (page: PAGE): number => {
|
||||
return this.currentPage[page];
|
||||
};
|
||||
|
||||
@action
|
||||
setCurrentPage = (page: PAGE, value: number) => {
|
||||
this.currentPage[page] = value;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,3 +46,11 @@ export const TABLE_COLUMN_MAX_WIDTH = 1500;
|
|||
|
||||
export const generateAssignToTeamInputDescription = (objectName: string): string =>
|
||||
`Assigning to a team allows you to filter ${objectName} and configure their visibility. Go to OnCall -> Settings -> Team and Access Settings for more details.`;
|
||||
|
||||
export enum PAGE {
|
||||
Integrations = 'integrations',
|
||||
Escalations = 'escalation_chains',
|
||||
Incidents = 'incidents',
|
||||
Webhooks = 'webhooks',
|
||||
Schedules = 'schedules',
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue