diff --git a/grafana-plugin/src/PluginPage.tsx b/grafana-plugin/src/PluginPage.tsx
index b8bce7a8..43e0d91f 100644
--- a/grafana-plugin/src/PluginPage.tsx
+++ b/grafana-plugin/src/PluginPage.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { PluginPageProps, PluginPage as RealPluginPage } from '@grafana/runtime';
import Header from 'navbar/Header/Header';
+import { pages } from 'pages';
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
import { useStore } from 'state/useStore';
import { useQueryParams } from 'utils/hooks';
@@ -18,6 +19,7 @@ function RealPlugin(props: PluginPageProps): React.ReactNode {
return (
+ {pages[page].text}
{props.children}
);
diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx
index 782ea07a..71c38142 100644
--- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx
+++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx
@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { SelectableValue } from '@grafana/data';
-import { getLocationSrv } from '@grafana/runtime';
import { Label, Button, HorizontalGroup, VerticalGroup, Select, LoadingPlaceholder } from '@grafana/ui';
import { capitalCase } from 'change-case';
import cn from 'classnames/bind';
@@ -19,6 +18,7 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_
import { Alert } from 'models/alertgroup/alertgroup.types';
import { makeRequest } from 'network';
import { UserAction } from 'state/userAction';
+import LocationHelper from 'utils/LocationHelper';
import styles from './AlertTemplatesForm.module.css';
@@ -162,9 +162,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
) : null}
);
- const handleGoToTemplateSettingsCllick = () => {
- getLocationSrv().update({ partial: true, query: { tab: 'Autoresolve' } });
- };
+ const handleGoToTemplateSettingsCllick = () => LocationHelper.update({ tab: 'Autoresolve' }, 'partial');
return (
diff --git a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx
index 224698dd..7644563c 100644
--- a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx
+++ b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx
@@ -36,7 +36,7 @@ export default function PageErrorHandlingWrapper({
objectName?: string;
pageName: string;
itemNotFoundMessage?: string;
- children: React.ReactNode;
+ children: () => React.ReactNode;
}): JSX.Element {
useEffect(() => {
if (!errorData) {
@@ -51,7 +51,7 @@ export default function PageErrorHandlingWrapper({
const store = useStore();
if (!errorData || !errorData.isWrongTeamError) {
- return <>{children}>;
+ return <>{children()}>;
}
const currentTeamId = store.userStore.currentUser?.current_team;
diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx
index 9f18e251..c98c58f7 100644
--- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx
+++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx
@@ -1,7 +1,6 @@
import plugin from '../../../package.json'; // eslint-disable-line
import React, { FC, useEffect, useState, useCallback } from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
@@ -12,6 +11,7 @@ import { getIfChatOpsConnected } from 'containers/DefaultPageLayout/helper';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
import { UserAction } from 'state/userAction';
+import LocationHelper from 'utils/LocationHelper';
import { GRAFANA_LICENSE_OSS } from 'utils/consts';
import { useForceUpdate } from 'utils/hooks';
import { getItem, setItem } from 'utils/localStorage';
@@ -46,7 +46,7 @@ const DefaultPageLayout: FC
= observer((props) => {
if (query.slack_error) {
setShowSlackInstallAlert(query.slack_error);
- getLocationSrv().update({ partial: true, query: { slack_error: undefined }, replace: true });
+ LocationHelper.update({ slack_error: undefined }, 'replace');
}
}, []);
diff --git a/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx b/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx
index 2d35e3d8..f6652400 100644
--- a/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx
+++ b/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx
@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Drawer, Tab, TabContent, TabsBar, Button, VerticalGroup, Input } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
@@ -15,6 +14,7 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_
import { Alert } from 'models/alertgroup/alertgroup.types';
import { useStore } from 'state/useStore';
import { openNotification } from 'utils';
+import LocationHelper from 'utils/LocationHelper';
import { IntegrationSettingsTab } from './IntegrationSettings.types';
import Autoresolve from './parts/Autoresolve';
@@ -46,7 +46,7 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
const getTabClickHandler = useCallback((tab: IntegrationSettingsTab) => {
return () => {
setActiveTab(tab);
- getLocationSrv().update({ partial: true, query: { tab: tab } });
+ LocationHelper.update({ tab }, 'partial');
};
}, []);
@@ -56,7 +56,7 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
useEffect(() => {
setActiveTab(startTab || IntegrationSettingsTab.Templates);
- getLocationSrv().update({ partial: true, query: { tab: startTab || IntegrationSettingsTab.Templates } });
+ LocationHelper.update({ tab: startTab || IntegrationSettingsTab.Templates }, 'partial');
}, [startTab]);
const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel);
diff --git a/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx b/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx
index 3fdf07bf..420c64a1 100644
--- a/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx
+++ b/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx
@@ -1,6 +1,5 @@
import React, { useCallback, useState, useEffect } from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Alert, Button, Icon, Label, Modal, Select } from '@grafana/ui';
import cn from 'classnames/bind';
import { get } from 'lodash-es';
@@ -15,6 +14,7 @@ import { Team } from 'models/team/team.types';
import { useStore } from 'state/useStore';
import { UserAction } from 'state/userAction';
import { openErrorNotification, openNotification } from 'utils';
+import LocationHelper from 'utils/LocationHelper';
import styles from 'containers/IntegrationSettings/parts/Autoresolve.module.css';
@@ -114,7 +114,7 @@ const Autoresolve = ({ alertReceiveChannelId, onSwitchToTemplate, alertGroupId }
};
const handleGoToTemplateSettingsCllick = () => {
- getLocationSrv().update({ partial: true, query: { tab: 'Templates' } });
+ LocationHelper.update({ tab: 'Templates' }, 'partial');
onSwitchToTemplate('resolve_condition_template');
};
diff --git a/grafana-plugin/src/img/grafanaGlobalStyles.css b/grafana-plugin/src/img/grafanaGlobalStyles.css
index 29ac020c..9e29d7ba 100644
--- a/grafana-plugin/src/img/grafanaGlobalStyles.css
+++ b/grafana-plugin/src/img/grafanaGlobalStyles.css
@@ -9,7 +9,7 @@
padding-bottom: 36px;
}
-.scrollbar-view [class*='-page-header'] {
+.scrollbar-view h1:first-child {
margin-bottom: 0 !important;
}
@@ -21,6 +21,7 @@
max-width: unset !important;
flex-grow: unset !important;
flex-basis: unset !important;
+ overflow-x: auto;
}
.page-scrollbar-content > div:first-child {
@@ -29,6 +30,7 @@
.page-header__title {
padding-top: 0 !important;
+ font-size: 2rem !important;
margin-right: 8px;
}
diff --git a/grafana-plugin/src/navbar/LegacyNavTabsBar.module.scss b/grafana-plugin/src/navbar/LegacyNavTabsBar.module.scss
new file mode 100644
index 00000000..c3b2ca3c
--- /dev/null
+++ b/grafana-plugin/src/navbar/LegacyNavTabsBar.module.scss
@@ -0,0 +1,3 @@
+.root {
+ min-width: 1500px;
+}
diff --git a/grafana-plugin/src/navbar/LegacyNavTabsBar.tsx b/grafana-plugin/src/navbar/LegacyNavTabsBar.tsx
index d96ba975..c6564d3c 100644
--- a/grafana-plugin/src/navbar/LegacyNavTabsBar.tsx
+++ b/grafana-plugin/src/navbar/LegacyNavTabsBar.tsx
@@ -2,10 +2,15 @@ import React from 'react';
import { IconName } from '@grafana/data';
import { Tab, TabsBar } from '@grafana/ui';
+import cn from 'classnames/bind';
import { pages } from 'pages';
import { useStore } from 'state/useStore';
+import styles from './LegacyNavTabsBar.module.scss';
+
+const cx = cn.bind(styles);
+
export default function LegacyNavTabsBar({ currentPage }: { currentPage: string }): JSX.Element {
const store = useStore();
@@ -14,7 +19,7 @@ export default function LegacyNavTabsBar({ currentPage }: { currentPage: string
.filter((page) => (page.hideFromTabsFn ? !page.hideFromTabsFn(store) : !page.hideFromTabs));
return (
-
+
{navigationPages.map((page, index) => (
{
- getLocationSrv().update({ partial: true, query: { id: escalationChain } });
+ LocationHelper.update({ id: escalationChain }, 'partial');
if (escalationChain) {
escalationChainStore.updateEscalationChainDetails(escalationChain);
}
@@ -143,79 +142,81 @@ class EscalationChainsPage extends React.Component
- <>
-
-
-
-
- {!searchResult || searchResult.length ? (
-
-
-
- {
- this.setState({ showCreateEscalationChainModal: true });
- }}
- icon="plus"
- className={cx('new-escalation-chain')}
- >
- New escalation chain
-
-
-
- {searchResult ? (
-
- {(item) => }
-
- ) : (
-
- )}
-
-
-
{this.renderEscalation()}
+ {() => (
+ <>
+
+
+
- ) : (
-
- No escalations found, check your filtering and current team.
-
+ {!searchResult || searchResult.length ? (
+
+
+
{
this.setState({ showCreateEscalationChainModal: true });
}}
+ icon="plus"
+ className={cx('new-escalation-chain')}
>
New Escalation Chain
-
- }
+
+ {searchResult ? (
+
+ {(item) => }
+
+ ) : (
+
+ )}
+
+
+
{this.renderEscalation()}
+
+ ) : (
+
+ No escalations found, check your filtering and current team.
+
+ {
+ this.setState({ showCreateEscalationChainModal: true });
+ }}
+ >
+ New Escalation Chain
+
+
+
+ }
+ />
+ )}
+
+ {showCreateEscalationChainModal && (
+
{
+ this.setState({
+ showCreateEscalationChainModal: false,
+ escalationChainIdToCopy: undefined,
+ });
+ }}
+ onUpdate={this.handleEscalationChainCreate}
/>
)}
-
- {showCreateEscalationChainModal && (
-
{
- this.setState({
- showCreateEscalationChainModal: false,
- escalationChainIdToCopy: undefined,
- });
- }}
- onUpdate={this.handleEscalationChainCreate}
- />
- )}
- >
+ >
+ )}
);
diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx
index 2125e374..c4f48065 100644
--- a/grafana-plugin/src/pages/incident/Incident.tsx
+++ b/grafana-plugin/src/pages/incident/Incident.tsx
@@ -1,6 +1,5 @@
import React, { useState, SyntheticEvent } from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import {
Button,
HorizontalGroup,
@@ -22,7 +21,6 @@ import moment from 'moment-timezone';
import CopyToClipboard from 'react-copy-to-clipboard';
import Emoji from 'react-emoji-render';
import reactStringReplace from 'react-string-replace';
-import { AppRootProps } from 'types';
import Collapse from 'components/Collapse/Collapse';
import Block from 'components/GBlock/Block';
@@ -49,12 +47,12 @@ import {
} from 'models/alertgroup/alertgroup.types';
import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types';
import { pages } from 'pages';
-import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
-import { WithStoreProps } from 'state/types';
+import { PageProps, WithStoreProps } from 'state/types';
import { useStore } from 'state/useStore';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
import { openNotification } from 'utils';
+import LocationHelper from 'utils/LocationHelper';
import sanitize from 'utils/sanitize';
import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from './Incident.helpers';
@@ -63,7 +61,7 @@ import styles from './Incident.module.css';
const cx = cn.bind(styles);
-interface IncidentPageProps extends WithStoreProps, AppRootProps {}
+interface IncidentPageProps extends WithStoreProps, PageProps {}
interface IncidentPageState extends PageBaseState {
showIntegrationSettings?: boolean;
@@ -97,8 +95,10 @@ class IncidentPage extends React.Component
update = () => {
this.setState({ errorData: initErrorDataState() }); // reset wrong team error to false
- const { store } = this.props;
- const { id } = getQueryParams();
+ const {
+ store,
+ query: { id },
+ } = this.props;
store.alertGroupStore
.getAlert(id)
@@ -106,8 +106,10 @@ class IncidentPage extends React.Component
};
render() {
- const { store } = this.props;
- const { id, cursor, start, perpage } = getQueryParams();
+ const {
+ store,
+ query: { id, cursor, start, perpage },
+ } = this.props;
const { errorData, showIntegrationSettings, showAttachIncidentForm } = this.state;
const { isNotFoundError, isWrongTeamError } = errorData;
@@ -127,74 +129,78 @@ class IncidentPage extends React.Component
return (
-
- {errorData.isNotFoundError ? (
-
-
- 404
- Incident not found
-
-
- Go to incidents page
-
-
-
-
- ) : (
- <>
- {this.renderHeader()}
-
-
-
{this.renderTimeline()}
+ {() => (
+
+ {errorData.isNotFoundError ? (
+
+
+ 404
+ Incident not found
+
+
+ Go to incidents page
+
+
+
- {showIntegrationSettings && (
-
{
- alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id);
- }}
- onUpdateTemplates={() => {
- store.alertGroupStore.getAlert(id);
- }}
- startTab={IntegrationSettingsTab.Templates}
- id={incident.alert_receive_channel.id}
- onHide={() =>
- this.setState({
- showIntegrationSettings: undefined,
- })
- }
- />
- )}
- {showAttachIncidentForm && (
- {
- this.setState({
- showAttachIncidentForm: false,
- });
- }}
- onUpdate={this.update}
- />
- )}
- >
- )}
-
+ ) : (
+ <>
+ {this.renderHeader()}
+
+
+
{this.renderTimeline()}
+
+ {showIntegrationSettings && (
+
{
+ alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id);
+ }}
+ onUpdateTemplates={() => {
+ store.alertGroupStore.getAlert(id);
+ }}
+ startTab={IntegrationSettingsTab.Templates}
+ id={incident.alert_receive_channel.id}
+ onHide={() =>
+ this.setState({
+ showIntegrationSettings: undefined,
+ })
+ }
+ />
+ )}
+ {showAttachIncidentForm && (
+ {
+ this.setState({
+ showAttachIncidentForm: false,
+ });
+ }}
+ onUpdate={this.update}
+ />
+ )}
+ >
+ )}
+
+ )}
);
}
renderHeader = () => {
- const { store } = this.props;
+ const {
+ store,
+ query: { id, cursor, start, perpage },
+ } = this.props;
- const { id, cursor, start, perpage } = getQueryParams();
const { alerts } = store.alertGroupStore;
const incident = alerts.get(id);
@@ -311,9 +317,11 @@ class IncidentPage extends React.Component
};
renderTimeline = () => {
- const { store } = this.props;
+ const {
+ store,
+ query: { id },
+ } = this.props;
- const { id } = getQueryParams();
const incident = store.alertGroupStore.alerts.get(id);
if (!incident.render_after_resolve_report_json) {
@@ -401,9 +409,11 @@ class IncidentPage extends React.Component
};
handleCreateResolutionNote = () => {
- const { store } = this.props;
+ const {
+ store,
+ query: { id },
+ } = this.props;
- const { id } = getQueryParams();
const { resolutionNoteText } = this.state;
store.resolutionNotesStore
.createResolutionNote(id, resolutionNoteText)
@@ -419,9 +429,7 @@ class IncidentPage extends React.Component
case 'author':
return (
{
- getLocationSrv().update({ query: { page: 'users', id: entity?.author?.pk } });
- }}
+ onClick={() => LocationHelper.update({ id: entity?.author?.pk, page: 'users' }, 'replace')}
style={{ textDecoration: 'underline', cursor: 'pointer' }}
>
{entity.author?.username}
diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx
index 3ef595a6..198c2da4 100644
--- a/grafana-plugin/src/pages/incidents/Incidents.tsx
+++ b/grafana-plugin/src/pages/incidents/Incidents.tsx
@@ -1,6 +1,5 @@
import React, { ReactElement, SyntheticEvent } from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Button, Icon, Tooltip, VerticalGroup, LoadingPlaceholder, HorizontalGroup } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
@@ -8,12 +7,10 @@ import { get } from 'lodash-es';
import { observer } from 'mobx-react';
import moment from 'moment-timezone';
import Emoji from 'react-emoji-render';
-import { AppRootProps } from 'types';
import CursorPagination from 'components/CursorPagination/CursorPagination';
import GTable from 'components/GTable/GTable';
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
-import PageErrorHandlingWrapper from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import PluginLink from 'components/PluginLink/PluginLink';
import Text from 'components/Text/Text';
import Tutorial from 'components/Tutorial/Tutorial';
@@ -25,11 +22,11 @@ import { Alert, Alert as AlertType, AlertAction } from 'models/alertgroup/alertg
import { User } from 'models/user/user.types';
import { pages } from 'pages';
import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from 'pages/incident/Incident.helpers';
-import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
import { move } from 'state/helpers';
-import { WithStoreProps } from 'state/types';
+import { PageProps, WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
+import LocationHelper from 'utils/LocationHelper';
import SilenceDropdown from './parts/SilenceDropdown';
@@ -54,7 +51,7 @@ function withSkeleton(fn: (alert: AlertType) => ReactElement | ReactElement[]) {
return WithSkeleton;
}
-interface IncidentsPageProps extends WithStoreProps, AppRootProps {}
+interface IncidentsPageProps extends WithStoreProps, PageProps {}
interface IncidentsPageState {
selectedIncidentIds: Array;
@@ -71,8 +68,10 @@ class Incidents extends React.Component
constructor(props: IncidentsPageProps) {
super(props);
- const { store } = props;
- const { cursor: cursorQuery, start: startQuery, perpage: perpageQuery } = getQueryParams();
+ const {
+ store,
+ query: { cursor: cursorQuery, start: startQuery, perpage: perpageQuery },
+ } = props;
const cursor = cursorQuery || undefined;
const start = !isNaN(startQuery) ? Number(startQuery) : 1;
@@ -103,12 +102,10 @@ class Incidents extends React.Component
render() {
return (
-
-
- {this.renderIncidentFilters()}
- {this.renderTable()}
-
-
+
+ {this.renderIncidentFilters()}
+ {this.renderTable()}
+
);
}
@@ -148,7 +145,7 @@ class Incidents extends React.Component
fetchIncidentData = (filters: IncidentsFiltersType, isOnMount: boolean) => {
const { store } = this.props;
store.alertGroupStore.updateIncidentFilters(filters, isOnMount); // this line fetches incidents
- getLocationSrv().update({ query: { page: 'incidents', ...store.alertGroupStore.incidentFilters } });
+ LocationHelper.update({ page: 'incidents', ...store.alertGroupStore.incidentFilters }, 'partial');
};
onChangeCursor = (cursor: string, direction: 'prev' | 'next') => {
@@ -577,7 +574,9 @@ class Incidents extends React.Component
}
setPollingInterval(filters: IncidentsFiltersType = this.state.filters, isOnMount = false) {
- this.pollingIntervalId = setInterval(() => this.fetchIncidentData(filters, isOnMount), POLLING_NUM_SECONDS * 1000);
+ this.pollingIntervalId = setInterval(() => {
+ this.fetchIncidentData(filters, isOnMount);
+ }, POLLING_NUM_SECONDS * 1000);
}
}
diff --git a/grafana-plugin/src/pages/index.tsx b/grafana-plugin/src/pages/index.tsx
index b3fa2499..a837581a 100644
--- a/grafana-plugin/src/pages/index.tsx
+++ b/grafana-plugin/src/pages/index.tsx
@@ -101,7 +101,7 @@ export const pages: { [id: string]: PageDefinition } = [
{
icon: 'cog',
id: 'settings',
- text: 'Organization Settings',
+ text: 'Settings',
hideFromBreadcrumbs: true,
path: getPath('settings'),
},
diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx
index e3a759b4..a5fb20c4 100644
--- a/grafana-plugin/src/pages/integrations/Integrations.tsx
+++ b/grafana-plugin/src/pages/integrations/Integrations.tsx
@@ -1,12 +1,10 @@
import React from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Button, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { debounce } from 'lodash-es';
import { observer } from 'mobx-react';
-import { AppRootProps } from 'types';
import GList from 'components/GList/GList';
import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/IntegrationsFilters';
@@ -27,9 +25,10 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
import { AlertReceiveChannel } from 'models/alert_receive_channel';
import { AlertReceiveChannelOption } from 'models/alert_receive_channel/alert_receive_channel.types';
import { pages } from 'pages';
-import { WithStoreProps } from 'state/types';
+import { PageProps, WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
+import LocationHelper from 'utils/LocationHelper';
import styles from './Integrations.module.css';
@@ -42,7 +41,7 @@ interface IntegrationsState extends PageBaseState {
integrationSettingsTab?: IntegrationSettingsTab;
}
-interface IntegrationsProps extends WithStoreProps, AppRootProps {}
+interface IntegrationsProps extends WithStoreProps, PageProps {}
@observer
class Integrations extends React.Component {
@@ -62,7 +61,7 @@ class Integrations extends React.Component
setSelectedAlertReceiveChannel = (alertReceiveChannelId: AlertReceiveChannel['id']) => {
const { store } = this.props;
store.selectedAlertReceiveChannel = alertReceiveChannelId;
- getLocationSrv().update({ partial: true, query: { id: alertReceiveChannelId } });
+ LocationHelper.update({ id: alertReceiveChannelId }, 'partial');
};
parseQueryParams = async () => {
@@ -139,110 +138,112 @@ class Integrations extends React.Component
pageName="integrations"
itemNotFoundMessage={`Integration with id=${query?.id} is not found. Please select integration from the list.`}
>
- <>
-
-
-
-
- {searchResult?.length ? (
-
-
-
- {
- this.setState({ showCreateIntegrationModal: true });
- }}
- icon="plus"
- className={cx('newIntegrationButton')}
- >
- New integration for receiving alerts
-
-
-
-
- {(item) => (
- {
- this.setState({
- alertReceiveChannelToShowSettings: item.id,
- integrationSettingsTab: IntegrationSettingsTab.Heartbeat,
- });
- }}
- />
- )}
-
-
-
-
-
{
- this.setState({
- alertReceiveChannelToShowSettings: store.selectedAlertReceiveChannel,
- integrationSettingsTab,
- });
- }}
- />
-
+ {() => (
+ <>
+
+
+
- ) : searchResult ? (
-
- No integrations found. Review your filter and team settings.
+ {searchResult?.length ? (
+
+
{
this.setState({ showCreateIntegrationModal: true });
}}
+ icon="plus"
+ className={cx('newIntegrationButton')}
>
New integration for receiving alerts
-
- }
+
+
+ {(item) => (
+ {
+ this.setState({
+ alertReceiveChannelToShowSettings: item.id,
+ integrationSettingsTab: IntegrationSettingsTab.Heartbeat,
+ });
+ }}
+ />
+ )}
+
+
+
+
+
{
+ this.setState({
+ alertReceiveChannelToShowSettings: store.selectedAlertReceiveChannel,
+ integrationSettingsTab,
+ });
+ }}
+ />
+
+
+ ) : searchResult ? (
+
+ No integrations found. Review your filter and team settings.
+
+ {
+ this.setState({ showCreateIntegrationModal: true });
+ }}
+ >
+ New integration for receiving alerts
+
+
+
+ }
+ />
+ ) : (
+
+ )}
+
+ {alertReceiveChannelToShowSettings && (
+
{
+ alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings);
+ }}
+ startTab={integrationSettingsTab}
+ id={alertReceiveChannelToShowSettings}
+ onHide={() => {
+ this.setState({
+ alertReceiveChannelToShowSettings: undefined,
+ integrationSettingsTab: undefined,
+ });
+ LocationHelper.update({ tab: undefined }, 'partial');
+ }}
/>
- ) : (
-
)}
-
- {alertReceiveChannelToShowSettings && (
-
{
- alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings);
- }}
- startTab={integrationSettingsTab}
- id={alertReceiveChannelToShowSettings}
- onHide={() => {
- this.setState({
- alertReceiveChannelToShowSettings: undefined,
- integrationSettingsTab: undefined,
- });
- getLocationSrv().update({ partial: true, query: { tab: undefined } });
- }}
- />
- )}
- {showCreateIntegrationModal && (
- {
- this.setState({ showCreateIntegrationModal: false });
- }}
- onCreate={this.handleCreateNewAlertReceiveChannel}
- />
- )}
- >
+ {showCreateIntegrationModal && (
+ {
+ this.setState({ showCreateIntegrationModal: false });
+ }}
+ onCreate={this.handleCreateNewAlertReceiveChannel}
+ />
+ )}
+ >
+ )}
);
diff --git a/grafana-plugin/src/pages/maintenance/Maintenance.tsx b/grafana-plugin/src/pages/maintenance/Maintenance.tsx
index 4d329475..9ec72ea3 100644
--- a/grafana-plugin/src/pages/maintenance/Maintenance.tsx
+++ b/grafana-plugin/src/pages/maintenance/Maintenance.tsx
@@ -7,7 +7,6 @@ import { observer } from 'mobx-react';
import moment from 'moment-timezone';
import LegacyNavHeading from 'navbar/LegacyNavHeading';
import Emoji from 'react-emoji-render';
-import { AppRootProps } from 'types';
import GTable from 'components/GTable/GTable';
import Text from 'components/Text/Text';
@@ -18,7 +17,7 @@ import { getAlertReceiveChannelDisplayName } from 'models/alert_receive_channel/
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { Maintenance, MaintenanceMode, MaintenanceType } from 'models/maintenance/maintenance.types';
import { pages } from 'pages';
-import { WithStoreProps } from 'state/types';
+import { PageProps, WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
@@ -26,7 +25,7 @@ import styles from './Maintenance.module.css';
const cx = cn.bind(styles);
-interface MaintenancePageProps extends AppRootProps, WithStoreProps {}
+interface MaintenancePageProps extends PageProps, WithStoreProps {}
interface MaintenancePageState {
maintenanceData?: {
diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx
new file mode 100644
index 00000000..463b3256
--- /dev/null
+++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx
@@ -0,0 +1,86 @@
+import 'jest/matchMedia.ts';
+import React from 'react';
+
+import { describe, expect, test } from '@jest/globals';
+import { render, screen, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import outgoingWebhooksStub from 'jest/outgoingWebhooksStub';
+
+import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
+import { OutgoingWebhooks } from 'pages/outgoing_webhooks/OutgoingWebhooks';
+
+const outgoingWebhooks = outgoingWebhooksStub as OutgoingWebhook[];
+const outgoingWebhookStore = () => ({
+ loadItem: () => Promise.resolve(outgoingWebhooks[0]),
+ updateItems: () => Promise.resolve(),
+ getSearchResult: () => outgoingWebhooks,
+ items: outgoingWebhooks.reduce((prev, current) => {
+ prev[current.id] = current;
+ return prev;
+ }, {}),
+});
+
+jest.mock('plugin/GrafanaPluginRootPage.helpers', () => ({
+ isTopNavbar: () => false,
+}));
+
+jest.mock('@grafana/runtime', () => ({
+ config: {
+ featureToggles: {
+ topNav: false,
+ },
+ },
+}));
+
+jest.mock('state/useStore', () => ({
+ useStore: () => ({
+ outgoingWebhookStore: outgoingWebhookStore(),
+ isUserActionAllowed: jest.fn().mockReturnValue(true),
+ }),
+}));
+
+jest.mock('@grafana/runtime', () => ({
+ getLocationSrv: jest.fn(),
+}));
+
+describe('OutgoingWebhooks', () => {
+ const storeMock = {
+ isUserActionAllowed: jest.fn().mockReturnValue(true),
+ outgoingWebhookStore: outgoingWebhookStore(),
+ };
+
+ beforeAll(() => {
+ console.warn = () => {};
+ console.error = () => {};
+ });
+
+ test('It renders all retrieved webhooks', async () => {
+ render( );
+
+ await waitFor(() => {
+ const gTable = screen.queryByTestId('test__gTable');
+ const rows = gTable.querySelectorAll('tbody tr');
+
+ expect(() => queryEditForm()).toThrow(); // edit doesn't show for [id=undefined]
+ expect(rows.length).toBe(outgoingWebhooks.length);
+ });
+ });
+
+ test('It opens Edit View if [id] is supplied', async () => {
+ const id = outgoingWebhooks[0].id;
+ render( );
+
+ expect(() => queryEditForm()).toThrow(); // before updates kick in
+ await waitFor(() => {
+ expect(queryEditForm()).toBeDefined(); // edit shows for [id=?]
+ });
+ });
+
+ function getProps(id: OutgoingWebhook['id'] = undefined): any {
+ return { store: storeMock, query: { id } };
+ }
+
+ function queryEditForm(): HTMLElement {
+ return screen.getByTestId('test__outgoingWebhookEditForm');
+ }
+});
diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx
index 797fc6d6..1ff3f4d7 100644
--- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx
+++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx
@@ -1,12 +1,10 @@
import React from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Button, HorizontalGroup } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import LegacyNavHeading from 'navbar/LegacyNavHeading';
-import { AppRootProps } from 'types';
import GTable from 'components/GTable/GTable';
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
@@ -22,16 +20,16 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
import { ActionDTO } from 'models/action';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { pages } from 'pages';
-import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
-import { WithStoreProps } from 'state/types';
+import { PageProps, WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
+import LocationHelper from 'utils/LocationHelper';
import styles from './OutgoingWebhooks.module.css';
const cx = cn.bind(styles);
-interface OutgoingWebhooksProps extends WithStoreProps, AppRootProps {}
+interface OutgoingWebhooksProps extends WithStoreProps, PageProps {}
interface OutgoingWebhooksState extends PageBaseState {
outgoingWebhookIdToEdit?: OutgoingWebhook['id'] | 'new';
@@ -43,14 +41,12 @@ class OutgoingWebhooks extends React.Component
- <>
-
-
(
-
-
- Outgoing Webhooks
-
-
-
-
-
- Create
-
-
-
+ {() => (
+ <>
+
+
(
+
+
+ Outgoing Webhooks
+
+
-
- )}
- rowKey="id"
- columns={columns}
- data={webhooks}
- />
-
- {outgoingWebhookIdToEdit && (
-
- )}
- >
+ )}
+ rowKey="id"
+ columns={columns}
+ data={webhooks}
+ />
+
+ {outgoingWebhookIdToEdit && (
+
+ )}
+ >
+ )}
);
@@ -194,14 +192,14 @@ class OutgoingWebhooks extends React.Component {
this.setState({ outgoingWebhookIdToEdit: id });
- getLocationSrv().update({ partial: true, query: { id } });
+ LocationHelper.update({ id }, 'partial');
};
};
handleOutgoingWebhookFormHide = () => {
this.setState({ outgoingWebhookIdToEdit: undefined });
- getLocationSrv().update({ partial: true, query: { id: undefined } });
+ LocationHelper.update({ id: undefined }, 'partial');
};
}
diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx
index 7f99d9fe..87e02706 100644
--- a/grafana-plugin/src/pages/schedule/Schedule.tsx
+++ b/grafana-plugin/src/pages/schedule/Schedule.tsx
@@ -1,12 +1,10 @@
import React from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Button, HorizontalGroup, VerticalGroup, IconButton, ToolbarButton, Icon, Modal } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
-import { AppRootProps } from 'types';
import PageErrorHandlingWrapper from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import PluginLink from 'components/PluginLink/PluginLink';
@@ -23,10 +21,10 @@ import UsersTimezones from 'containers/UsersTimezones/UsersTimezones';
import { Schedule, ScheduleType, Shift } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { pages } from 'pages';
-import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
-import { WithStoreProps } from 'state/types';
+import { PageProps, WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
+import LocationHelper from 'utils/LocationHelper';
import { getStartOfWeek } from './Schedule.helpers';
@@ -34,7 +32,7 @@ import styles from './Schedule.module.css';
const cx = cn.bind(styles);
-interface SchedulePageProps extends AppRootProps, WithStoreProps {}
+interface SchedulePageProps extends PageProps, WithStoreProps {}
interface SchedulePageState {
startMoment: dayjs.Dayjs;
@@ -66,8 +64,10 @@ class SchedulePage extends React.Component
}
async componentDidMount() {
- const { store } = this.props;
- const { id } = getQueryParams();
+ const {
+ store,
+ query: { id },
+ } = this.props;
store.userStore.updateItems();
@@ -86,8 +86,10 @@ class SchedulePage extends React.Component
}
render() {
- const { store } = this.props;
- const { id: scheduleId } = getQueryParams();
+ const {
+ store,
+ query: { id: scheduleId },
+ } = this.props;
const {
startMoment,
@@ -112,139 +114,150 @@ class SchedulePage extends React.Component
return (
-
-
-
-
-
-
-
-
-
- {schedule?.name}
-
- {schedule && }
-
-
- {users && (
+ {() => (
+ <>
+
+
+
+
- Current timezone:
-
+
+
+
+
+ {schedule?.name}
+
+ {schedule && }
- )}
-
-
-
- Export
-
- {(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && (
-
- Reload
-
+
+ {users && (
+
+ Current timezone:
+
+
)}
+
+
+
+ Export
+
+ {(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && (
+
+ Reload
+
+ )}
+
+ {
+ this.setState({ showEditForm: true });
+ }}
+ />
+
+
+
+
- {
- this.setState({ showEditForm: true });
- }}
- />
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
- Today
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Today
+
+
+
+
+
+
+
+
+
+
+ {startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
+
+
-
- {startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- {showEditForm && (
- {
- this.setState({ showEditForm: false });
- }}
- />
- )}
- {showScheduleICalSettings && (
- this.setState({ showScheduleICalSettings: false })}
- >
-
-
+ {showEditForm && (
+ {
+ this.setState({ showEditForm: false });
+ }}
+ />
+ )}
+ {showScheduleICalSettings && (
+ this.setState({ showScheduleICalSettings: false })}
+ >
+
+
+ )}
+ >
)}
@@ -293,8 +306,10 @@ class SchedulePage extends React.Component
};
updateEvents = () => {
- const { store } = this.props;
- const { id: scheduleId } = getQueryParams();
+ const {
+ store,
+ query: { id: scheduleId },
+ } = this.props;
const { startMoment } = this.state;
@@ -418,12 +433,12 @@ class SchedulePage extends React.Component
};
handleDelete = () => {
- const { store } = this.props;
- const { id: scheduleId } = getQueryParams();
+ const {
+ store,
+ query: { id: scheduleId },
+ } = this.props;
- store.scheduleStore.delete(scheduleId).then(() => {
- getLocationSrv().update({ query: { page: 'schedules' } });
- });
+ store.scheduleStore.delete(scheduleId).then(() => LocationHelper.update({ page: 'schedules' }, 'replace'));
};
}
diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx
index 7b7cdbd1..18d6ca52 100644
--- a/grafana-plugin/src/pages/schedules/Schedules.tsx
+++ b/grafana-plugin/src/pages/schedules/Schedules.tsx
@@ -1,6 +1,5 @@
import React from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
@@ -31,6 +30,7 @@ import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
import { WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
+import LocationHelper from 'utils/LocationHelper';
import styles from './Schedules.module.css';
@@ -210,7 +210,7 @@ class SchedulesPage extends React.Component {
if (data.type === ScheduleType.API) {
- getLocationSrv().update({ query: { page: 'schedule', id: data.id } });
+ LocationHelper.update({ page: 'schedule', id: data.id }, 'replace');
}
};
@@ -259,9 +259,7 @@ class SchedulesPage extends React.Component {
- return () => {
- getLocationSrv().update({ query: { page: 'schedule', id: scheduleId } });
- };
+ return () => LocationHelper.update({ page: 'schedule', id: scheduleId }, 'replace');
};
renderType = (value: number) => {
diff --git a/grafana-plugin/src/pages/settings/tabs/Cloud/CloudPage.tsx b/grafana-plugin/src/pages/settings/tabs/Cloud/CloudPage.tsx
index 2675f113..17ebd610 100644
--- a/grafana-plugin/src/pages/settings/tabs/Cloud/CloudPage.tsx
+++ b/grafana-plugin/src/pages/settings/tabs/Cloud/CloudPage.tsx
@@ -1,6 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Field, Input, Button, HorizontalGroup, Icon, VerticalGroup, LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
@@ -15,6 +14,7 @@ import { WithStoreProps } from 'state/types';
import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore';
import { openErrorNotification } from 'utils';
+import LocationHelper from 'utils/LocationHelper';
import styles from './CloudPage.module.css';
@@ -116,7 +116,7 @@ const CloudPage = observer((_props: CloudPageProps) => {
variant="secondary"
size="sm"
className={cx('table-button')}
- onClick={() => getLocationSrv().update({ query: { page: 'users', p: page, id: user.id } })}
+ onClick={() => LocationHelper.update({ page: 'users', p: page, id: user.id }, 'replace')}
>
Configure notifications
diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx
index 623b379b..6faa3b55 100644
--- a/grafana-plugin/src/pages/users/Users.tsx
+++ b/grafana-plugin/src/pages/users/Users.tsx
@@ -1,13 +1,11 @@
import React from 'react';
-import { getLocationSrv } from '@grafana/runtime';
import { Alert, Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { debounce } from 'lodash-es';
import { observer } from 'mobx-react';
import LegacyNavHeading from 'navbar/LegacyNavHeading';
-import { AppRootProps } from 'types';
import Avatar from 'components/Avatar/Avatar';
import GTable from 'components/GTable/GTable';
@@ -24,10 +22,10 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
import { getRole } from 'models/user/user.helpers';
import { User as UserType, UserRole } from 'models/user/user.types';
import { pages } from 'pages';
-import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
-import { WithStoreProps } from 'state/types';
+import { PageProps, WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
+import LocationHelper from 'utils/LocationHelper';
import { getRealFilters, getUserRowClassNameFn } from './Users.helpers';
@@ -35,7 +33,7 @@ import styles from './Users.module.css';
const cx = cn.bind(styles);
-interface UsersProps extends WithStoreProps, AppRootProps {}
+interface UsersProps extends WithStoreProps, PageProps {}
const ITEMS_PER_PAGE = 100;
@@ -65,10 +63,10 @@ class Users extends React.Component {
initialUsersLoaded = false;
- private userId: string;
-
async componentDidMount() {
- const { p } = getQueryParams();
+ const {
+ query: { p },
+ } = this.props;
this.setState({ page: p ? Number(p) : 1 }, this.updateUsers);
this.parseParams();
@@ -83,11 +81,11 @@ class Users extends React.Component {
return;
}
- getLocationSrv().update({ query: { p: page }, partial: true });
+ LocationHelper.update({ p: page }, 'partial');
return await userStore.updateItems(getRealFilters(usersFilters), page);
};
- componentDidUpdate() {
+ componentDidUpdate(prevProps: UsersProps) {
const { store } = this.props;
if (!this.initialUsersLoaded && store.isUserActionAllowed(UserAction.ViewOtherUsers)) {
@@ -95,7 +93,7 @@ class Users extends React.Component {
this.initialUsersLoaded = true;
}
- if (this.userId !== getQueryParams()['id']) {
+ if (prevProps.query.id !== this.props.query.id) {
this.parseParams();
}
}
@@ -103,10 +101,10 @@ class Users extends React.Component {
parseParams = async () => {
this.setState({ errorData: initErrorDataState() }); // reset wrong team error to false on query parse
- const { store } = this.props;
- const { id } = getQueryParams();
-
- this.userId = id;
+ const {
+ store,
+ query: { id },
+ } = this.props;
if (id) {
await (id === 'me' ? store.userStore.loadCurrentUser() : store.userStore.loadUser(String(id), true)).catch(
@@ -182,74 +180,76 @@ class Users extends React.Component {
pageName="users"
itemNotFoundMessage={`User with id=${query?.id} is not found. Please select user from the list.`}
>
- <>
-
-
-
-
-
-
- Users
-
-
- To manage permissions or add users, please visit{' '}
- Grafana user management
-
+ {() => (
+ <>
+
+
+
-
-
- View my profile
-
-
-
- {store.isUserActionAllowed(UserAction.ViewOtherUsers) ? (
- <>
-
-
-
- Clear filters
+
+
+ View my profile
-
+
+
+ {store.isUserActionAllowed(UserAction.ViewOtherUsers) ? (
+ <>
+
+
+
+ Clear filters
+
+
-
+ >
+ ) : (
+
+ You don't have enough permissions to view other users because you are not Admin.{' '}
+ Click here to open your profile
+ >
+ }
+ severity="info"
/>
- >
- ) : (
-
- You don't have enough permissions to view other users because you are not Admin.{' '}
- Click here to open your profile
- >
- }
- severity="info"
- />
- )}
+ )}
+
+ {userPkToEdit &&
}
- {userPkToEdit &&
}
-
- >
+ >
+ )}
);
@@ -378,7 +378,7 @@ class Users extends React.Component
{
handleHideUserSettings = () => {
this.setState({ userPkToEdit: undefined });
- getLocationSrv().update({ partial: true, query: { id: undefined } });
+ LocationHelper.update({ id: undefined }, 'partial');
};
handleUserUpdate = () => {
diff --git a/grafana-plugin/src/plugin.json b/grafana-plugin/src/plugin.json
index d753a8e6..93e971a1 100644
--- a/grafana-plugin/src/plugin.json
+++ b/grafana-plugin/src/plugin.json
@@ -34,6 +34,7 @@
"name": "Alert Groups",
"path": "/a/grafana-oncall-app/?page=incidents",
"role": "Viewer",
+ "defaultNav": true,
"addToNav": true
},
{
diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx
index 23854334..63b461be 100644
--- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx
+++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx
@@ -8,7 +8,17 @@ export function getQueryParams(): any {
const searchParams = new URLSearchParams(window.location.search);
const result = {};
for (const [key, value] of searchParams) {
- result[key] = value;
+ if (result[key]) {
+ // key already existing, we're handling an array
+ if (!Array.isArray(result[key])) {
+ result[key] = new Array(result[key]);
+ }
+
+ result[key].push(value);
+ } else {
+ result[key] = value;
+ }
}
+
return result;
}
diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx
index d2b4cd59..43288bb3 100644
--- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx
+++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx
@@ -35,7 +35,7 @@ import 'style/vars.css';
import 'style/global.css';
import 'style/utils.css';
-import { isTopNavbar } from './GrafanaPluginRootPage.helpers';
+import { getQueryParams, isTopNavbar } from './GrafanaPluginRootPage.helpers';
import PluginSetup from './PluginSetup';
export const GrafanaPluginRootPage = (props: AppRootProps) => (
@@ -101,7 +101,7 @@ export const Root = observer((props: AppRootProps) => {
'u-position-relative'
)}
>
-
+
);
diff --git a/grafana-plugin/src/state/types.ts b/grafana-plugin/src/state/types.ts
index 642df664..9f9eccbd 100644
--- a/grafana-plugin/src/state/types.ts
+++ b/grafana-plugin/src/state/types.ts
@@ -1,9 +1,16 @@
+import { AppPluginMeta, KeyValue } from '@grafana/data';
+
import { RootStore } from 'state/index';
export interface WithStoreProps {
store: RootStore;
}
+export interface PageProps
{
+ meta: AppPluginMeta;
+ query: KeyValue;
+}
+
export interface SelectOption {
value: string | number;
display_name: string;
diff --git a/grafana-plugin/src/style/global.css b/grafana-plugin/src/style/global.css
index 07a8af39..fe3e65a2 100644
--- a/grafana-plugin/src/style/global.css
+++ b/grafana-plugin/src/style/global.css
@@ -39,3 +39,7 @@
.navbarRootFallback {
margin-top: 24px;
}
+
+.page-title {
+ margin-bottom: 16px;
+}
diff --git a/grafana-plugin/src/utils/LocationHelper.ts b/grafana-plugin/src/utils/LocationHelper.ts
new file mode 100644
index 00000000..27bfe38f
--- /dev/null
+++ b/grafana-plugin/src/utils/LocationHelper.ts
@@ -0,0 +1,43 @@
+import { KeyValue } from '@grafana/data';
+import { locationService } from '@grafana/runtime';
+
+import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
+
+class LocationHelper {
+ update(params: KeyValue, method: 'replace' | 'push' | 'partial') {
+ const queryParams = getQueryParams();
+
+ const sortedExistingParams = sort(queryParams);
+ const sortedNewParams = sort(params);
+
+ if (toQueryString(sortedExistingParams) !== toQueryString(sortedNewParams)) {
+ if (method === 'partial') {
+ locationService.partial(params);
+ } else {
+ locationService[method](toQueryString(sortedNewParams));
+ }
+ }
+ }
+}
+
+function toQueryString(queryParams: KeyValue) {
+ const urlParams = new URLSearchParams(queryParams);
+ for (const [key, value] of Object.entries(queryParams)) {
+ if (Array.isArray(value)) {
+ urlParams.delete(key);
+ value.forEach((v) => urlParams.append(key, v));
+ }
+ }
+ return urlParams.toString();
+}
+
+function sort(object: KeyValue) {
+ return Object.keys(object)
+ .sort()
+ .reduce((obj, key) => {
+ obj[key] = object[key];
+ return obj;
+ }, {});
+}
+
+export default new LocationHelper();