From 2fa22b7d3287f27a114dc497a86af86bd7652a96 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 17 Nov 2022 13:09:33 +0200 Subject: [PATCH 01/12] move call of getQueryParams back to PluginRoot and rely on this.props.query instead --- .../UsersTimezones/UsersTimezones.tsx | 6 +++- .../src/pages/incident/Incident.tsx | 36 +++++++++++-------- .../src/pages/incidents/Incidents.tsx | 12 +++---- .../outgoing_webhooks/OutgoingWebhooks.tsx | 20 +++++------ .../src/pages/schedule/Schedule.tsx | 36 +++++++++++-------- grafana-plugin/src/pages/users/Users.tsx | 24 ++++++------- .../src/plugin/GrafanaPluginRootPage.tsx | 4 +-- grafana-plugin/src/state/types.ts | 6 ++++ 8 files changed, 82 insertions(+), 62 deletions(-) diff --git a/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx index b364eca3..d61b7882 100644 --- a/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx +++ b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx @@ -51,7 +51,11 @@ const UsersTimezones: FC = (props) => { [userIds, store.userStore.items] ); - const currentMoment = useMemo(() => dayjs().tz(tz), [tz]); + const currentMoment = useMemo(() => { + console.log({ tz }); + console.log(dayjs().format()); + return dayjs().tz(tz); + }, [tz]); const currentTimeX = useMemo(() => { const midnight = dayjs().tz(tz).startOf('day'); diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index c6377277..585da6d5 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 { AppRootProps } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; import { Button, @@ -49,8 +48,7 @@ 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'; @@ -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; @@ -192,9 +194,11 @@ class IncidentPage extends React.Component } 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 +315,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 +407,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) diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 718251b5..1c24889f 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 { AppRootProps } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; import { Button, Icon, Tooltip, VerticalGroup, LoadingPlaceholder, HorizontalGroup } from '@grafana/ui'; import { PluginPage } from 'PluginPage'; @@ -25,9 +24,8 @@ 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'; @@ -54,7 +52,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 +69,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; diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 6435f10e..c12ff9dd 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { AppRootProps } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; import { Button, HorizontalGroup } from '@grafana/ui'; import { PluginPage } from 'PluginPage'; @@ -22,8 +21,7 @@ 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'; @@ -31,7 +29,7 @@ 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 } async componentDidMount() { - const { store } = this.props; - const { id } = getQueryParams(); + const { + store, + query: { id }, + } = this.props; store.userStore.updateItems(); @@ -87,8 +87,10 @@ class SchedulePage extends React.Component } render() { - const { store } = this.props; - const { id: scheduleId } = getQueryParams(); + const { + store, + query: { id: scheduleId }, + } = this.props; const { startMoment, @@ -299,8 +301,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; @@ -424,8 +428,10 @@ class SchedulePage extends React.Component }; updateEventsFor = async (scheduleId: Schedule['id'], withEmpty = true, with_gap = true) => { - const { store } = this.props; - const { id } = getQueryParams(); + const { + store, + query: { id }, + } = this.props; const { scheduleStore } = store; @@ -444,8 +450,10 @@ 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' } }); diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index c4a6458d..2e93a12b 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { AppRootProps } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; import { Alert, Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui'; import { PluginPage } from 'PluginPage'; @@ -24,8 +23,7 @@ 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'; @@ -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(); @@ -87,7 +85,7 @@ class Users extends React.Component { 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( diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx index 416b0767..a1565375 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx @@ -37,7 +37,7 @@ import 'style/vars.css'; import 'style/global.css'; import 'style/utils.css'; -import { isTopNavbar } from './GrafanaPluginRootPage.helpers'; +import { getQueryParams, isTopNavbar } from './GrafanaPluginRootPage.helpers'; export const GrafanaPluginRootPage = (props: AppRootProps) => ( @@ -156,7 +156,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..5cbc2fa6 100644 --- a/grafana-plugin/src/state/types.ts +++ b/grafana-plugin/src/state/types.ts @@ -1,9 +1,15 @@ +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; From 5ed143051526bcb63d593e0916619e5d98cc3ac4 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 17 Nov 2022 13:44:47 +0200 Subject: [PATCH 02/12] replace deprecated usage of getLocationSrv() in favor of locationService --- .../AlertTemplates/AlertTemplatesForm.tsx | 6 +-- .../DefaultPageLayout/DefaultPageLayout.tsx | 4 +- .../IntegrationSettings.tsx | 6 +-- .../IntegrationSettings/parts/Autoresolve.tsx | 4 +- .../UsersTimezones/UsersTimezones.tsx | 6 +-- .../escalation-chains/EscalationChains.tsx | 4 +- .../src/pages/incident/Incident.tsx | 6 +-- .../src/pages/incidents/Incidents.tsx | 4 +- .../src/pages/integrations/Integrations.tsx | 6 +-- .../outgoing_webhooks/OutgoingWebhooks.tsx | 6 +-- .../src/pages/schedule/Schedule.tsx | 6 +-- .../src/pages/schedules/Schedules.tsx | 8 ++-- .../pages/settings/tabs/Cloud/CloudPage.tsx | 4 +- grafana-plugin/src/pages/users/Users.tsx | 6 +-- grafana-plugin/src/utils/LocationHelper.ts | 40 +++++++++++++++++++ 15 files changed, 72 insertions(+), 44 deletions(-) create mode 100644 grafana-plugin/src/utils/LocationHelper.ts diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx index 782ea07a..1e9d812a 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'; @@ -21,6 +20,7 @@ import { makeRequest } from 'network'; import { UserAction } from 'state/userAction'; import styles from './AlertTemplatesForm.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -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/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 75d796fb..872384ba 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -2,7 +2,6 @@ import plugin from '../../../package.json'; // eslint-disable-line import React, { FC, useEffect, useState, useCallback } from 'react'; import { AppRootProps } from '@grafana/data'; -import { getLocationSrv } from '@grafana/runtime'; import { Alert } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; @@ -20,6 +19,7 @@ import sanitize from 'utils/sanitize'; import { getSlackMessage } from './DefaultPageLayout.helpers'; import styles from './DefaultPageLayout.module.scss'; import { SlackError } from './DefaultPageLayout.types'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -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..f69dbff8 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'; @@ -20,6 +19,7 @@ import { IntegrationSettingsTab } from './IntegrationSettings.types'; import Autoresolve from './parts/Autoresolve'; import styles from 'containers/IntegrationSettings/IntegrationSettings.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -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..e2f10650 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'; @@ -17,6 +16,7 @@ import { UserAction } from 'state/userAction'; import { openErrorNotification, openNotification } from 'utils'; import styles from 'containers/IntegrationSettings/parts/Autoresolve.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -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/containers/UsersTimezones/UsersTimezones.tsx b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx index d61b7882..b364eca3 100644 --- a/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx +++ b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx @@ -51,11 +51,7 @@ const UsersTimezones: FC = (props) => { [userIds, store.userStore.items] ); - const currentMoment = useMemo(() => { - console.log({ tz }); - console.log(dayjs().format()); - return dayjs().tz(tz); - }, [tz]); + const currentMoment = useMemo(() => dayjs().tz(tz), [tz]); const currentTimeX = useMemo(() => { const midnight = dayjs().tz(tz).startOf('day'); diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 081daf14..52e3ec01 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { AppRootProps } from '@grafana/data'; -import { getLocationSrv } from '@grafana/runtime'; import { Button, HorizontalGroup, Icon, IconButton, LoadingPlaceholder, Tooltip, VerticalGroup } from '@grafana/ui'; import { PluginPage } from 'PluginPage'; import cn from 'classnames/bind'; @@ -33,6 +32,7 @@ import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import styles from './EscalationChains.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -103,7 +103,7 @@ class EscalationChainsPage extends React.Component { - getLocationSrv().update({ partial: true, query: { id: escalationChain } }); + LocationHelper.update({ id: escalationChain }, 'partial'); if (escalationChain) { escalationChainStore.updateEscalationChainDetails(escalationChain); } diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 585da6d5..c8d8bc0b 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, @@ -58,6 +57,7 @@ import sanitize from 'utils/sanitize'; import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from './Incident.helpers'; import styles from './Incident.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -427,9 +427,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 1c24889f..aa7bf867 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'; @@ -32,6 +31,7 @@ import { withMobXProviderContext } from 'state/withStore'; import SilenceDropdown from './parts/SilenceDropdown'; import styles from './Incidents.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -148,7 +148,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 }, 'replace'); }; onChangeCursor = (cursor: string, direction: 'prev' | 'next') => { diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index 21d6d7a9..ddb4774a 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { AppRootProps } from '@grafana/data'; -import { getLocationSrv } from '@grafana/runtime'; import { Button, LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; import { PluginPage } from 'PluginPage'; import cn from 'classnames/bind'; @@ -32,6 +31,7 @@ import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import styles from './Integrations.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -62,7 +62,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 () => { @@ -230,7 +230,7 @@ class Integrations extends React.Component alertReceiveChannelToShowSettings: undefined, integrationSettingsTab: undefined, }); - getLocationSrv().update({ partial: true, query: { tab: undefined } }); + LocationHelper.update({ tab: undefined }, 'partial'); }} /> )} diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index c12ff9dd..bb30c811 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { getLocationSrv } from '@grafana/runtime'; import { Button, HorizontalGroup } from '@grafana/ui'; import { PluginPage } from 'PluginPage'; import cn from 'classnames/bind'; @@ -26,6 +25,7 @@ import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import styles from './OutgoingWebhooks.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -190,14 +190,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 78ce3037..b3c7e3e2 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -1,6 +1,5 @@ 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'; @@ -30,6 +29,7 @@ import { withMobXProviderContext } from 'state/withStore'; import { getStartOfWeek } from './Schedule.helpers'; import styles from './Schedule.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -455,9 +455,7 @@ class SchedulePage extends React.Component 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..9c853186 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'; @@ -33,6 +32,7 @@ import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import styles from './Schedules.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -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..e94419a0 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'; @@ -17,6 +16,7 @@ import { withMobXProviderContext } from 'state/withStore'; import { openErrorNotification } from 'utils'; import styles from './CloudPage.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -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 2e93a12b..1f943fe2 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -1,6 +1,5 @@ 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'; @@ -30,6 +29,7 @@ import { withMobXProviderContext } from 'state/withStore'; import { getRealFilters, getUserRowClassNameFn } from './Users.helpers'; import styles from './Users.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -81,7 +81,7 @@ 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); }; @@ -376,7 +376,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/utils/LocationHelper.ts b/grafana-plugin/src/utils/LocationHelper.ts new file mode 100644 index 00000000..653db17c --- /dev/null +++ b/grafana-plugin/src/utils/LocationHelper.ts @@ -0,0 +1,40 @@ +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 (getPathFromQueryParams(sortedExistingParams) !== getPathFromQueryParams(sortedNewParams)) { + if (method === 'partial') { + locationService.partial(params); + } else { + locationService[method](getPathFromQueryParams(sortedNewParams)); + } + } + } +} + +function getPathFromQueryParams(queryParams) { + return Object.keys(queryParams) + .map((key) => `${key}=${queryParams[key]}`) + .reduce((result, param, index) => { + const delimitator = `${index > 0 ? '&' : ''}`; + return `${result}${delimitator}${param}`; + }, '?'); +} + +function sort(object: KeyValue) { + return Object.keys(object) + .sort() + .reduce((obj, key) => { + obj[key] = object[key]; + return obj; + }, {}); +} + +export default new LocationHelper(); From 265c78f3427f905de54de3195567e8c0e249c93b Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 17 Nov 2022 13:58:34 +0200 Subject: [PATCH 03/12] show header in 9.3 --- grafana-plugin/src/PluginPage.tsx | 2 ++ grafana-plugin/src/img/grafanaGlobalStyles.css | 3 ++- grafana-plugin/src/style/global.css | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/grafana-plugin/src/PluginPage.tsx b/grafana-plugin/src/PluginPage.tsx index b8bce7a8..d18b4155 100644 --- a/grafana-plugin/src/PluginPage.tsx +++ b/grafana-plugin/src/PluginPage.tsx @@ -6,6 +6,7 @@ import Header from 'navbar/Header/Header'; import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers'; import { useStore } from 'state/useStore'; import { useQueryParams } from 'utils/hooks'; +import { pages } from 'pages'; export const PluginPage = (isTopNavbar() ? RealPlugin : PluginPageFallback) as React.ComponentType; @@ -18,6 +19,7 @@ function RealPlugin(props: PluginPageProps): React.ReactNode { return (
+

{pages[page].text}

{props.children} ); diff --git a/grafana-plugin/src/img/grafanaGlobalStyles.css b/grafana-plugin/src/img/grafanaGlobalStyles.css index 29ac020c..c8a3defd 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; } @@ -29,6 +29,7 @@ .page-header__title { padding-top: 0 !important; + font-size: 2rem !important; margin-right: 8px; } diff --git a/grafana-plugin/src/style/global.css b/grafana-plugin/src/style/global.css index 07a8af39..e7590d45 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; +} \ No newline at end of file From b9b6d2d5a71d8a3b74f9e49a0b15cb1b2e66d00a Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Thu, 17 Nov 2022 13:59:57 +0200 Subject: [PATCH 04/12] bring back test for outgoing webhooks --- grafana-plugin/src/PluginPage.tsx | 2 +- .../AlertTemplates/AlertTemplatesForm.tsx | 2 +- .../DefaultPageLayout/DefaultPageLayout.tsx | 2 +- .../IntegrationSettings.tsx | 2 +- .../IntegrationSettings/parts/Autoresolve.tsx | 2 +- .../escalation-chains/EscalationChains.tsx | 2 +- .../src/pages/incident/Incident.tsx | 2 +- .../src/pages/incidents/Incidents.tsx | 2 +- .../src/pages/integrations/Integrations.tsx | 2 +- .../OutgoingWebhooks.test.tsx | 86 +++++++++++++++++++ .../outgoing_webhooks/OutgoingWebhooks.tsx | 2 +- .../src/pages/schedule/Schedule.tsx | 2 +- .../src/pages/schedules/Schedules.tsx | 2 +- .../pages/settings/tabs/Cloud/CloudPage.tsx | 2 +- grafana-plugin/src/pages/users/Users.tsx | 2 +- grafana-plugin/src/state/types.ts | 3 +- grafana-plugin/src/style/global.css | 2 +- grafana-plugin/src/utils/LocationHelper.ts | 1 + 18 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx diff --git a/grafana-plugin/src/PluginPage.tsx b/grafana-plugin/src/PluginPage.tsx index d18b4155..43e0d91f 100644 --- a/grafana-plugin/src/PluginPage.tsx +++ b/grafana-plugin/src/PluginPage.tsx @@ -3,10 +3,10 @@ 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'; -import { pages } from 'pages'; export const PluginPage = (isTopNavbar() ? RealPlugin : PluginPageFallback) as React.ComponentType; diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx index 1e9d812a..71c38142 100644 --- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx +++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx @@ -18,9 +18,9 @@ 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'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 872384ba..f5b266c5 100644 --- a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx +++ b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx @@ -11,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'; @@ -19,7 +20,6 @@ import sanitize from 'utils/sanitize'; import { getSlackMessage } from './DefaultPageLayout.helpers'; import styles from './DefaultPageLayout.module.scss'; import { SlackError } from './DefaultPageLayout.types'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx b/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx index f69dbff8..f6652400 100644 --- a/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx +++ b/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx @@ -14,12 +14,12 @@ 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'; import styles from 'containers/IntegrationSettings/IntegrationSettings.module.css'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx b/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx index e2f10650..420c64a1 100644 --- a/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx +++ b/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx @@ -14,9 +14,9 @@ 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'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 52e3ec01..42f66cd7 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -30,9 +30,9 @@ import { pages } from 'pages'; import { WithStoreProps } from 'state/types'; import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; +import LocationHelper from 'utils/LocationHelper'; import styles from './EscalationChains.module.css'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index c8d8bc0b..528c28e0 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -52,12 +52,12 @@ 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'; import styles from './Incident.module.css'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index aa7bf867..5b6e0014 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -27,11 +27,11 @@ import { move } from 'state/helpers'; 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'; import styles from './Incidents.module.css'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index ddb4774a..e2d5ee5b 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -29,9 +29,9 @@ import { pages } from 'pages'; import { 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'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); 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..c2c9f9f4 --- /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(); + + const gTable = screen.queryByTestId('test__gTable'); + const rows = gTable.querySelectorAll('tbody tr'); + + await waitFor(() => { + 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 bb30c811..68bc5e76 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -23,9 +23,9 @@ import { pages } from 'pages'; 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'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index b3c7e3e2..424c9920 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -25,11 +25,11 @@ import { pages } from 'pages'; 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'; import styles from './Schedule.module.css'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 9c853186..18d6ca52 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -30,9 +30,9 @@ 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'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/settings/tabs/Cloud/CloudPage.tsx b/grafana-plugin/src/pages/settings/tabs/Cloud/CloudPage.tsx index e94419a0..17ebd610 100644 --- a/grafana-plugin/src/pages/settings/tabs/Cloud/CloudPage.tsx +++ b/grafana-plugin/src/pages/settings/tabs/Cloud/CloudPage.tsx @@ -14,9 +14,9 @@ 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'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index 1f943fe2..dfd44a3d 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -25,11 +25,11 @@ import { pages } from 'pages'; 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'; import styles from './Users.module.css'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/state/types.ts b/grafana-plugin/src/state/types.ts index 5cbc2fa6..9f9eccbd 100644 --- a/grafana-plugin/src/state/types.ts +++ b/grafana-plugin/src/state/types.ts @@ -1,4 +1,5 @@ import { AppPluginMeta, KeyValue } from '@grafana/data'; + import { RootStore } from 'state/index'; export interface WithStoreProps { @@ -7,7 +8,7 @@ export interface WithStoreProps { export interface PageProps { meta: AppPluginMeta; - query: KeyValue + query: KeyValue; } export interface SelectOption { diff --git a/grafana-plugin/src/style/global.css b/grafana-plugin/src/style/global.css index e7590d45..fe3e65a2 100644 --- a/grafana-plugin/src/style/global.css +++ b/grafana-plugin/src/style/global.css @@ -42,4 +42,4 @@ .page-title { margin-bottom: 16px; -} \ No newline at end of file +} diff --git a/grafana-plugin/src/utils/LocationHelper.ts b/grafana-plugin/src/utils/LocationHelper.ts index 653db17c..ce233afd 100644 --- a/grafana-plugin/src/utils/LocationHelper.ts +++ b/grafana-plugin/src/utils/LocationHelper.ts @@ -1,5 +1,6 @@ import { KeyValue } from '@grafana/data'; import { locationService } from '@grafana/runtime'; + import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers'; class LocationHelper { From 75c7d2bb56a69fe25cac11278d9c181bf75e3713 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 21 Nov 2022 11:23:05 +0200 Subject: [PATCH 05/12] bring back children arrow return in PageErrorHandlingWrapper to prevent NPE errors --- .../PageErrorHandlingWrapper.tsx | 2 +- .../escalation-chains/EscalationChains.tsx | 128 ++++---- .../src/pages/incident/Incident.tsx | 118 ++++---- .../src/pages/incidents/Incidents.tsx | 10 +- .../src/pages/integrations/Integrations.tsx | 186 ++++++------ .../outgoing_webhooks/OutgoingWebhooks.tsx | 74 ++--- .../src/pages/schedule/Schedule.tsx | 273 +++++++++--------- grafana-plugin/src/pages/users/Users.tsx | 128 ++++---- 8 files changed, 471 insertions(+), 448 deletions(-) diff --git a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx index 224698dd..51407f7a 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) { diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 42f66cd7..ce34d031 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -143,79 +143,81 @@ class EscalationChainsPage extends React.Component - <> -
-
- -
- {!searchResult || searchResult.length ? ( -
-
- - - -
- {searchResult ? ( - - {(item) => } - - ) : ( - - )} -
-
-
{this.renderEscalation()}
+ {() => ( + <> +
+
+
- ) : ( - - No escalations found, check your filtering and current team. - + {!searchResult || searchResult.length ? ( +
+
+ - - } +
+ {searchResult ? ( + + {(item) => } + + ) : ( + + )} +
+
+
{this.renderEscalation()}
+
+ ) : ( + + No escalations found, check your filtering and current team. + + + + + } + /> + )} +
+ {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 528c28e0..c4f48065 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -129,65 +129,67 @@ class IncidentPage extends React.Component return ( -
- {errorData.isNotFoundError ? ( -
- - 404 - Incident not found - - - - -
- ) : ( - <> - {this.renderHeader()} -
-
- - - -
-
{this.renderTimeline()}
+ {() => ( +
+ {errorData.isNotFoundError ? ( +
+ + 404 + Incident not found + + + +
- {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} + /> + )} + + )} +
+ )} ); diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 5b6e0014..a72fdc05 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -104,10 +104,12 @@ class Incidents extends React.Component return ( -
- {this.renderIncidentFilters()} - {this.renderTable()} -
+ {() => ( +
+ {this.renderIncidentFilters()} + {this.renderTable()} +
+ )}
); diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index e2d5ee5b..2f73a788 100644 --- a/grafana-plugin/src/pages/integrations/Integrations.tsx +++ b/grafana-plugin/src/pages/integrations/Integrations.tsx @@ -139,110 +139,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 ? ( -
-
- - - -
- - {(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 ? ( +
+
- - } +
+ + {(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. + + + + + } + /> + ) : ( + + )} +
+ {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, - }); - LocationHelper.update({ tab: undefined }, 'partial'); - }} - /> - )} - {showCreateIntegrationModal && ( - { - this.setState({ showCreateIntegrationModal: false }); - }} - onCreate={this.handleCreateNewAlertReceiveChannel} - /> - )} - + {showCreateIntegrationModal && ( + { + this.setState({ showCreateIntegrationModal: false }); + }} + onCreate={this.handleCreateNewAlertReceiveChannel} + /> + )} + + )} ); diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 68bc5e76..1ff3f4d7 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -118,43 +118,45 @@ class OutgoingWebhooks extends React.Component - <> -
- ( -
- - Outgoing Webhooks - -
- - - - - + {() => ( + <> +
+ ( +
+ + Outgoing Webhooks + +
+ + + + + +
-
- )} - rowKey="id" - columns={columns} - data={webhooks} - /> -
- {outgoingWebhookIdToEdit && ( - - )} - + )} + rowKey="id" + columns={columns} + data={webhooks} + /> +
+ {outgoingWebhookIdToEdit && ( + + )} + + )} ); diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 424c9920..c7e0489d 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -115,144 +115,155 @@ class SchedulePage extends React.Component return ( -
- -
- - - - - - - {schedule?.name} - - {schedule && } - - - {users && ( + {() => ( + <> +
+ +
+ - Current timezone: - + + + + + {schedule?.name} + + {schedule && } - )} - - {schedule?.type === ScheduleType.Ical && ( + + {users && ( + + Current timezone: + + + )} - - + {schedule?.type === ScheduleType.Ical && ( + + + + + )} + { + this.setState({ showEditForm: true }); + }} + /> + + + - )} - { - this.setState({ showEditForm: true }); - }} - /> - - - - - - -
- {schedule?.type !== ScheduleType.API && ( - - Ical and API/Terraform schedules are read-only - - )} -
- -
- -
-
- - - - - - - - {startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')} - - -
- - - +
+ {schedule?.type !== ScheduleType.API && ( + + Ical and API/Terraform schedules are read-only + + )} +
+ +
+ +
+
+ + + + + + + + + {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 })} + > + + + )} + )} diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index dfd44a3d..6faa3b55 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -180,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 - + {() => ( + <> +
+
+
+
+
+ + Users + + + To manage permissions or add users, please visit{' '} + Grafana user management + +
-
- - - -
- {store.isUserActionAllowed(UserAction.ViewOtherUsers) ? ( - <> -
- - -
+ +
+ {store.isUserActionAllowed(UserAction.ViewOtherUsers) ? ( + <> +
+ + +
- + + ) : ( + + 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 && } -
- + + )} ); From 6be6a4289e7d73c09f4d55399457161e73a35140 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 21 Nov 2022 11:24:15 +0200 Subject: [PATCH 06/12] call fn --- .../PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx index 51407f7a..7644563c 100644 --- a/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx +++ b/grafana-plugin/src/components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.tsx @@ -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; From 47c80eb3f5ebadc84a4b6c540d0e5f79d3abc075 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 21 Nov 2022 11:26:41 +0200 Subject: [PATCH 07/12] include queryByTestId within await call --- .../src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx index c2c9f9f4..463b3256 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.test.tsx @@ -57,10 +57,10 @@ describe('OutgoingWebhooks', () => { test('It renders all retrieved webhooks', async () => { render(); - const gTable = screen.queryByTestId('test__gTable'); - const rows = gTable.querySelectorAll('tbody tr'); - 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); }); From 4d1da2ecd2bedfa4c9e6600460c4b9fc6b89777d Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 22 Nov 2022 14:37:18 +0200 Subject: [PATCH 08/12] improved toQueryParams version, fixed infinite reload on Incidents due to using 'replace' instead of 'partial' --- .../src/pages/incidents/Incidents.tsx | 6 ++++-- grafana-plugin/src/utils/LocationHelper.ts | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index a72fdc05..ac6df386 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -150,7 +150,7 @@ class Incidents extends React.Component fetchIncidentData = (filters: IncidentsFiltersType, isOnMount: boolean) => { const { store } = this.props; store.alertGroupStore.updateIncidentFilters(filters, isOnMount); // this line fetches incidents - LocationHelper.update({ page: 'incidents', ...store.alertGroupStore.incidentFilters }, 'replace'); + LocationHelper.update({ page: 'incidents', ...store.alertGroupStore.incidentFilters }, 'partial'); }; onChangeCursor = (cursor: string, direction: 'prev' | 'next') => { @@ -579,7 +579,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/utils/LocationHelper.ts b/grafana-plugin/src/utils/LocationHelper.ts index ce233afd..27bfe38f 100644 --- a/grafana-plugin/src/utils/LocationHelper.ts +++ b/grafana-plugin/src/utils/LocationHelper.ts @@ -10,23 +10,25 @@ class LocationHelper { const sortedExistingParams = sort(queryParams); const sortedNewParams = sort(params); - if (getPathFromQueryParams(sortedExistingParams) !== getPathFromQueryParams(sortedNewParams)) { + if (toQueryString(sortedExistingParams) !== toQueryString(sortedNewParams)) { if (method === 'partial') { locationService.partial(params); } else { - locationService[method](getPathFromQueryParams(sortedNewParams)); + locationService[method](toQueryString(sortedNewParams)); } } } } -function getPathFromQueryParams(queryParams) { - return Object.keys(queryParams) - .map((key) => `${key}=${queryParams[key]}`) - .reduce((result, param, index) => { - const delimitator = `${index > 0 ? '&' : ''}`; - return `${result}${delimitator}${param}`; - }, '?'); +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) { From 3ab15275efbeab5b2223f1b8732d4225d6386b15 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 23 Nov 2022 15:09:46 +0200 Subject: [PATCH 09/12] fix for routes & incidents list load --- grafana-plugin/src/pages/incidents/Incidents.tsx | 15 +++++---------- .../src/plugin/GrafanaPluginRootPage.helpers.tsx | 12 +++++++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index ac6df386..bc6d8b2c 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -11,7 +11,6 @@ import Emoji from 'react-emoji-render'; 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'; @@ -27,11 +26,11 @@ import { move } from 'state/helpers'; 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'; import styles from './Incidents.module.css'; +import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); @@ -103,14 +102,10 @@ class Incidents extends React.Component render() { return ( - - {() => ( -
- {this.renderIncidentFilters()} - {this.renderTable()} -
- )} -
+
+ {this.renderIncidentFilters()} + {this.renderTable()} +
); } diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx index 23854334..e8335720 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; } From af418573e380982263a915560a1c276348f0cf2a Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 23 Nov 2022 15:59:54 +0200 Subject: [PATCH 10/12] minor merge fixes --- .../DefaultPageLayout/DefaultPageLayout.tsx | 1 - .../escalation-chains/EscalationChains.tsx | 6 +- .../src/pages/incident/Incident.tsx | 1 - .../src/pages/incidents/Incidents.tsx | 3 +- .../src/pages/integrations/Integrations.tsx | 6 +- .../src/pages/maintenance/Maintenance.tsx | 5 +- .../outgoing_webhooks/OutgoingWebhooks.tsx | 1 - .../src/pages/schedule/Schedule.tsx | 83 +++++++++++-------- grafana-plugin/src/pages/users/Users.tsx | 1 - .../plugin/GrafanaPluginRootPage.helpers.tsx | 2 +- .../src/plugin/GrafanaPluginRootPage.tsx | 1 + 11 files changed, 56 insertions(+), 54 deletions(-) diff --git a/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx b/grafana-plugin/src/containers/DefaultPageLayout/DefaultPageLayout.tsx index 7828a7c6..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 { AppRootProps } from '@grafana/data'; import { Alert } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 0fe743af..e63befa9 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -1,12 +1,10 @@ import React from 'react'; -import { AppRootProps } from '@grafana/data'; import { Button, HorizontalGroup, Icon, IconButton, LoadingPlaceholder, Tooltip, 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 Collapse from 'components/Collapse/Collapse'; import EscalationsFilters from 'components/EscalationsFilters/EscalationsFilters'; @@ -28,7 +26,7 @@ import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChai import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { EscalationChain } from 'models/escalation_chain/escalation_chain.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'; @@ -37,7 +35,7 @@ import styles from './EscalationChains.module.css'; const cx = cn.bind(styles); -interface EscalationChainsPageProps extends WithStoreProps, AppRootProps {} +interface EscalationChainsPageProps extends WithStoreProps, PageProps {} interface EscalationChainsPageState extends PageBaseState { escalationChainsFilters: { searchTerm: string }; diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index c3b600c5..c4f48065 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -21,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'; diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 62259397..198c2da4 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -7,7 +7,6 @@ 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'; @@ -27,11 +26,11 @@ import { move } from 'state/helpers'; 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'; import styles from './Incidents.module.css'; -import LocationHelper from 'utils/LocationHelper'; const cx = cn.bind(styles); diff --git a/grafana-plugin/src/pages/integrations/Integrations.tsx b/grafana-plugin/src/pages/integrations/Integrations.tsx index ec212094..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 { AppRootProps } from '@grafana/data'; 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,7 +25,7 @@ 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'; @@ -43,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 { 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.tsx b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 0528e00c..1ff3f4d7 100644 --- a/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/grafana-plugin/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -5,7 +5,6 @@ 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'; diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 70e046f6..87e02706 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -5,7 +5,6 @@ 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'; @@ -135,44 +134,56 @@ class SchedulePage extends React.Component {schedule && } - )} - - - - {(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && ( - + + {users && ( + + Current timezone: + + )} + + + + {(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && ( + + )} + + { + this.setState({ showEditForm: true }); + }} + /> + + + + - { - this.setState({ showEditForm: true }); - }} - /> - - - - - -
-
- -
+
+
+ +
diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index fc4a04a6..6faa3b55 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -6,7 +6,6 @@ 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'; diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx index e8335720..63b461be 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.helpers.tsx @@ -13,7 +13,7 @@ export function getQueryParams(): any { if (!Array.isArray(result[key])) { result[key] = new Array(result[key]); } - + result[key].push(value); } else { result[key] = value; diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx index c6d2c335..43288bb3 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx @@ -36,6 +36,7 @@ import 'style/global.css'; import 'style/utils.css'; import { getQueryParams, isTopNavbar } from './GrafanaPluginRootPage.helpers'; +import PluginSetup from './PluginSetup'; export const GrafanaPluginRootPage = (props: AppRootProps) => ( From 987700bbf43df71a98d9c415dd6a1103c7cbb532 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 23 Nov 2022 17:04:27 +0200 Subject: [PATCH 11/12] make tabs horizontally scroll if <1500px --- grafana-plugin/src/img/grafanaGlobalStyles.css | 1 + grafana-plugin/src/navbar/LegacyNavTabsBar.module.scss | 3 +++ grafana-plugin/src/navbar/LegacyNavTabsBar.tsx | 7 ++++++- grafana-plugin/src/pages/index.tsx | 2 +- grafana-plugin/src/plugin.json | 1 + 5 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 grafana-plugin/src/navbar/LegacyNavTabsBar.module.scss diff --git a/grafana-plugin/src/img/grafanaGlobalStyles.css b/grafana-plugin/src/img/grafanaGlobalStyles.css index c8a3defd..9e29d7ba 100644 --- a/grafana-plugin/src/img/grafanaGlobalStyles.css +++ b/grafana-plugin/src/img/grafanaGlobalStyles.css @@ -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 { 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) => ( Date: Wed, 23 Nov 2022 23:43:21 +0200 Subject: [PATCH 12/12] fix casing --- grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index e63befa9..16a65c55 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -159,7 +159,7 @@ class EscalationChainsPage extends React.Component - New escalation chain + New Escalation Chain