diff --git a/CHANGELOG.md b/CHANGELOG.md index b4bec82a..d1229b14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/grafana-plugin/src/components/CursorPagination/CursorPagination.tsx b/grafana-plugin/src/components/CursorPagination/CursorPagination.tsx index 907f6a74..140a226d 100644 --- a/grafana-plugin/src/components/CursorPagination/CursorPagination.tsx +++ b/grafana-plugin/src/components/CursorPagination/CursorPagination.tsx @@ -24,7 +24,7 @@ const CursorPagination: FC = (props) => { setDisabled(false); }, [prev, next]); - const onChangeItemsPerPageCallback = useCallback((option) => { + const onChangeItemsPerPageCallback = useCallback((option: SelectableValue) => { setDisabled(true); onChangeItemsPerPage(option.value); }, []); diff --git a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.module.scss b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.module.scss index 9910b418..96b652bd 100644 --- a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.module.scss +++ b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.module.scss @@ -27,6 +27,10 @@ .integrationTree__group { position: relative; margin-bottom: 12px; + + &--hidden { + display: none; + } } .integrationTree__icon { diff --git a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx index 0375c963..97c0e7c4 100644 --- a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx +++ b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.tsx @@ -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; } -const IntegrationCollapsibleTreeView: React.FC = (props) => { +const IntegrationCollapsibleTreeView: React.FC = observer((props) => { const { configElements } = props; const [expandedList, setExpandedList] = useState(getStartingExpandedState()); @@ -97,7 +99,7 @@ const IntegrationCollapsibleTreeView: React.FC +
{item.canHoverIcon ? ( diff --git a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx index d7653c77..c386482f 100644 --- a/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx +++ b/grafana-plugin/src/components/IntegrationContactPoint/IntegrationContactPoint.tsx @@ -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<{
void; openEditTemplateModal: (templateName: string | string[], channelFilterId?: ChannelFilter['id']) => void; onEditRegexpTemplate: (channelFilterId: ChannelFilter['id']) => void; + onRouteDelete: (routeId: string) => void; + onItemMove: () => void; } const CollapsedIntegrationRouteDisplay: React.FC = 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(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 setRouteIdForDeletion(channelFilterId)} openRouteTemplateEditor={() => handleEditRoutingTemplate(channelFilter, channelFilterId)} /> @@ -179,8 +191,7 @@ const CollapsedIntegrationRouteDisplay: React.FC void; onEditRegexpTemplate: (channelFilterId: ChannelFilter['id']) => void; + onRouteDelete: (routeId: string) => void; + onItemMove: () => void; } interface ExpandedIntegrationRouteDisplayState { @@ -59,7 +61,16 @@ interface ExpandedIntegrationRouteDisplayState { } const ExpandedIntegrationRouteDisplay: React.FC = 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 setState({ routeIdForDeletion: channelFilterId })} openRouteTemplateEditor={() => handleEditRoutingTemplate(channelFilter, channelFilterId)} /> @@ -278,8 +290,7 @@ const ExpandedIntegrationRouteDisplay: React.FC = ({ @@ -327,6 +339,7 @@ export const RouteButtonsDisplay: React.FC = ({ routeIndex, setRouteIdForDeletion, openRouteTemplateEditor, + onItemMove, }) => { const { alertReceiveChannelStore } = useStore(); const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId]; @@ -404,11 +417,13 @@ export const RouteButtonsDisplay: React.FC = ({ 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(); } }; diff --git a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx index 6f8b92c6..37825c23 100644 --- a/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx +++ b/grafana-plugin/src/containers/RemoteFilters/RemoteFilters.tsx @@ -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 { } LocationHelper.update({ ...values }, 'partial'); + onChange(values, isOnMount); }; diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index 927922df..f5871d3e 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -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 { + 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 => makeRequest(`${this.path}${id}/start_maintenance/`, { method: 'POST', diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index 11fbe7fb..5586f496 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -111,35 +111,35 @@ export class UserStore extends BaseStore { } @action - async updateItems(f: any = { searchTerm: '' }, page = 1) { - return new Promise(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 { + 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() { diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 9262e038..faf5db74 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -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 diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index f8ed88e3..8148d9b6 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -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
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
{this.renderBulkActions()}
{ + if (!alertReceiveChannel) { + return false; + } + if (typeof alertReceiveChannel === 'string') { return alertReceiveChannel === 'grafana_alerting'; } diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index 0c9f37d8..afe9128b 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -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 { this.openEditTemplateModal(query.template, query.routeId && query.routeId); } - await this.loadIntegration(); + await this.loadData(); } render() { @@ -127,7 +127,7 @@ class Integration extends React.Component { } = this.state; const { store: { alertReceiveChannelStore }, - query: { p }, + query, match: { params: { id }, }, @@ -181,7 +181,7 @@ class Integration extends React.Component { )}
- +

@@ -200,7 +200,7 @@ class Integration extends React.Component { {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)}
{ To ensure a smooth transition you can migrate now using "Migrate" button in the menu on the right. - Please, check{' '} + Please check out the{' '} { } = 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 { }; 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 { 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 { 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 { 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 { templates={templates} openEditTemplateModal={this.openEditTemplateModal} onEditRegexpTemplate={this.handleEditRegexpRouteTemplate} + onItemMove={() => this.forceUpdate()} + onRouteDelete={onRouteDelete} /> ), } as IntegrationCollapsibleItem) @@ -655,7 +670,7 @@ class Integration extends React.Component { 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 { 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 { } 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 { }) .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 { diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 36d0b710..5b5cfcfd 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -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 { - 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 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
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, }} />
+ {alertReceiveChannelId && ( { @@ -209,21 +216,17 @@ class Integrations extends React.Component ); } - renderNotFound() { - return ( -
- Not found -
- ); - } - renderName = (item: AlertReceiveChannel) => { - const { - query: { p }, - } = this.props; + const { query } = this.props; return ( - + ]; }; + 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 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); diff --git a/grafana-plugin/src/pages/maintenance/Maintenance.module.css b/grafana-plugin/src/pages/maintenance/Maintenance.module.css deleted file mode 100644 index af825964..00000000 --- a/grafana-plugin/src/pages/maintenance/Maintenance.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.select { - width: 400px; -} - -.header { - display: flex; - justify-content: space-between; -} - -.title { - margin-bottom: var(--title-marginBottom); -} - -.info-box { - width: 100%; -} diff --git a/grafana-plugin/src/pages/maintenance/Maintenance.tsx b/grafana-plugin/src/pages/maintenance/Maintenance.tsx deleted file mode 100644 index a90d1561..00000000 --- a/grafana-plugin/src/pages/maintenance/Maintenance.tsx +++ /dev/null @@ -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 { - render() { - return ( - <> - - Maintenance mode is now controlled at the{' '} - Integration level. This page will soon be - removed. - - } - /> - - ); - } -} - -export default MaintenancePage; diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 899aeecd..230a4649 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -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 diff --git a/grafana-plugin/src/pages/routes.tsx b/grafana-plugin/src/pages/routes.tsx index e6b1480f..a224529d 100644 --- a/grafana-plugin/src/pages/routes.tsx +++ b/grafana-plugin/src/pages/routes.tsx @@ -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', diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 1db98490..99ae23ac 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -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 diff --git a/grafana-plugin/src/plugin.json b/grafana-plugin/src/plugin.json index 6e5a6ce8..2479e4c9 100644 --- a/grafana-plugin/src/plugin.json +++ b/grafana-plugin/src/plugin.json @@ -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", diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx index 59ede184..844f697e 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx @@ -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) => { - - - diff --git a/grafana-plugin/src/state/rootBaseStore/index.ts b/grafana-plugin/src/state/rootBaseStore/index.ts index ce2f0154..264cdca2 100644 --- a/grafana-plugin/src/state/rootBaseStore/index.ts +++ b/grafana-plugin/src/state/rootBaseStore/index.ts @@ -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; + }; } diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index 407864f0..0182abe7 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -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', +}