diff --git a/.config/.eslintrc b/.config/.eslintrc index 1486ed2e0e..04140192aa 100644 --- a/.config/.eslintrc +++ b/.config/.eslintrc @@ -14,9 +14,6 @@ { "plugins": ["deprecation"], "files": ["src/**/*.{ts,tsx}"], - "rules": { - "deprecation/deprecation": "warn" - }, "parserOptions": { "project": "./tsconfig.json" } diff --git a/.config/webpack/webpack.config.ts b/.config/webpack/webpack.config.ts index 93f20a24ee..521edc6333 100644 --- a/.config/webpack/webpack.config.ts +++ b/.config/webpack/webpack.config.ts @@ -199,7 +199,7 @@ const config = async (env): Promise => { if (isWSL()) { baseConfig.watchOptions = { - poll: 3000, + // poll: 3000, ignored: /node_modules/, }; } diff --git a/.eslintrc.js b/.eslintrc.js index 7d13aa6b98..a4d297ec69 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,7 +12,7 @@ module.exports = { { files: ['src/**/*.{ts,tsx}'], rules: { - 'deprecation/deprecation': 'off', + 'deprecation/deprecation': 'warn', }, parserOptions: { project: './tsconfig.json', diff --git a/package.json b/package.json index 4ab71ba8c9..4a11325e01 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "^18.0.6", "@types/react-responsive": "^8.0.5", - "@types/react-router-dom": "^5.3.3", "@types/react-test-renderer": "^18.0.5", "@types/react-transition-group": "^4.4.5", "@types/testing-library__jest-dom": "5.14.8", @@ -170,7 +169,7 @@ "react-hook-form": "^7.50.1", "react-modal": "^3.15.1", "react-responsive": "^8.1.0", - "react-router-dom": "5.3.3", + "react-router-dom-v5-compat": "^6.25.1", "react-sortable-hoc": "^1.11.0", "react-string-replace": "^0.4.4", "react-transition-group": "^4.4.5", diff --git a/src/components/PluginLink/PluginLink.tsx b/src/components/PluginLink/PluginLink.tsx index c9fd0ec589..a6d5d2cd1b 100644 --- a/src/components/PluginLink/PluginLink.tsx +++ b/src/components/PluginLink/PluginLink.tsx @@ -3,7 +3,7 @@ import React, { FC, useCallback, useMemo } from 'react'; import { css, cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import { bem } from 'styles/utils.styles'; import { getPathFromQueryParams } from 'utils/url'; diff --git a/src/containers/IntegrationForm/IntegrationForm.tsx b/src/containers/IntegrationForm/IntegrationForm.tsx index 39c58f6e88..92055ef281 100644 --- a/src/containers/IntegrationForm/IntegrationForm.tsx +++ b/src/containers/IntegrationForm/IntegrationForm.tsx @@ -19,7 +19,7 @@ import { } from '@grafana/ui'; import { observer } from 'mobx-react'; import { Controller, useForm, useFormContext, FormProvider } from 'react-hook-form'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { HowTheIntegrationWorks } from 'components/HowTheIntegrationWorks/HowTheIntegrationWorks'; import { PluginLink } from 'components/PluginLink/PluginLink'; @@ -94,7 +94,7 @@ export const IntegrationForm = observer( onBackClick, }: IntegrationFormProps) => { const store = useStore(); - const history = useHistory(); + const navigate = useNavigate(); const styles = useStyles2(getIntegrationFormStyles); const isNew = id === 'new'; const { @@ -453,7 +453,8 @@ export const IntegrationForm = observer( async function createNewIntegration(): Promise { const response = await alertReceiveChannelStore.create({ data, skipErrorHandling: true }); const pushHistory = (id: ApiSchemas['AlertReceiveChannel']['id']) => - history.push(`${PLUGIN_ROOT}/integrations/${id}`); + navigate(`${PLUGIN_ROOT}/integrations/${id}`); + if (!response) { return; } diff --git a/src/containers/MobileAppConnection/MobileAppConnection.test.tsx b/src/containers/MobileAppConnection/MobileAppConnection.test.tsx index 14c9327490..03cb1746ee 100644 --- a/src/containers/MobileAppConnection/MobileAppConnection.test.tsx +++ b/src/containers/MobileAppConnection/MobileAppConnection.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom-v5-compat'; import { UserHelper } from 'models/user/user.helpers'; import { ApiSchemas } from 'network/oncall-api/api.types'; diff --git a/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx b/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx index d323aa0f23..5413f640af 100644 --- a/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx +++ b/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx @@ -14,7 +14,7 @@ import { import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import { FormProvider, useForm, useFormContext } from 'react-hook-form'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { Text } from 'components/Text/Text'; import { OutgoingWebhookStatus } from 'containers/OutgoingWebhookStatus/OutgoingWebhookStatus'; @@ -304,7 +304,7 @@ interface EditWebhookTabsProps { const EditWebhookTabs = (props: EditWebhookTabsProps) => { const { id, data, action, onHide, onUpdate, onDelete, onSubmit, onTemplateEditClick, preset } = props; - const history = useHistory(); + const navigate = useNavigate(); const [activeTab, setActiveTab] = useState( action === WebhookFormActionType.EDIT_SETTINGS ? WebhookTabs.Settings.key : WebhookTabs.LastRun.key @@ -323,7 +323,7 @@ const EditWebhookTabs = (props: EditWebhookTabsProps) => { key={WebhookTabs.Settings.key} onChangeTab={() => { setActiveTab(WebhookTabs.Settings.key); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`); + navigate(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`); }} active={activeTab === WebhookTabs.Settings.key} label={WebhookTabs.Settings.value} @@ -333,7 +333,7 @@ const EditWebhookTabs = (props: EditWebhookTabsProps) => { key={WebhookTabs.LastRun.key} onChangeTab={() => { setActiveTab(WebhookTabs.LastRun.key); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`); + navigate(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`); }} active={activeTab === WebhookTabs.LastRun.key} label={WebhookTabs.LastRun.value} diff --git a/src/containers/PluginConfigPage/PluginConfigPage.test.tsx b/src/containers/PluginConfigPage/PluginConfigPage.test.tsx index 196ddc5017..219cb1a222 100644 --- a/src/containers/PluginConfigPage/PluginConfigPage.test.tsx +++ b/src/containers/PluginConfigPage/PluginConfigPage.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { useLocation as useLocationOriginal } from 'react-router-dom'; +import { useLocation as useLocationOriginal } from 'react-router-dom-v5-compat'; import { OnCallPluginConfigPageProps } from 'types'; import { PluginState } from 'state/plugin/plugin'; @@ -17,7 +17,7 @@ jest.mock('../../../package.json', () => ({ version: 'v1.2.3', })); -jest.mock('react-router-dom', () => ({ +jest.mock('react-router-dom-v5-compat', () => ({ useLocation: jest.fn(() => ({ search: '', })), diff --git a/src/containers/PluginConfigPage/PluginConfigPage.tsx b/src/containers/PluginConfigPage/PluginConfigPage.tsx index 428c10bfc1..d433ab79ef 100644 --- a/src/containers/PluginConfigPage/PluginConfigPage.tsx +++ b/src/containers/PluginConfigPage/PluginConfigPage.tsx @@ -1,7 +1,7 @@ import React, { FC, useCallback, useEffect, useState } from 'react'; import { Button, HorizontalGroup, Label, Legend, LinkButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom-v5-compat'; import { OnCallPluginConfigPageProps } from 'types'; import { PluginState, PluginStatusResponseBase } from 'state/plugin/plugin'; diff --git a/src/containers/Rotations/SchedulePersonal.tsx b/src/containers/Rotations/SchedulePersonal.tsx index 7b4833aa90..23aa3e19fd 100644 --- a/src/containers/Rotations/SchedulePersonal.tsx +++ b/src/containers/Rotations/SchedulePersonal.tsx @@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Badge, BadgeColor, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui'; import dayjs from 'dayjs'; import { observer } from 'mobx-react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { Avatar } from 'components/Avatar/Avatar'; @@ -32,14 +32,16 @@ import { getRotationsStyles } from './Rotations.styles'; import animationStyles from './Rotations.module.css'; -interface SchedulePersonalProps extends RouteComponentProps { +interface SchedulePersonalProps { userPk: ApiSchemas['User']['pk']; onSlotClick?: (event: Event) => void; theme: GrafanaTheme2; } -const _SchedulePersonal: FC = observer(({ userPk, onSlotClick, history }) => { +const _SchedulePersonal: FC = observer(({ userPk, onSlotClick }) => { const store = useStore(); + const navigate = useNavigate(); + const { timezoneStore, scheduleStore, userStore } = store; const updatePersonalEventsLoading = useIsLoading(ActionKey.UPDATE_PERSONAL_EVENTS); @@ -77,7 +79,7 @@ const _SchedulePersonal: FC = observer(({ userPk, onSlotC }; const openSchedule = (event: Event) => { - history.push(`${PLUGIN_ROOT}/schedules/${event.schedule?.id}`); + navigate(`${PLUGIN_ROOT}/schedules/${event.schedule?.id}`); }; const currentTimeX = getCurrentTimeX( @@ -172,4 +174,4 @@ const _SchedulePersonal: FC = observer(({ userPk, onSlotC ); }); -export const SchedulePersonal = withRouter(withTheme2(_SchedulePersonal)); +export const SchedulePersonal = withTheme2(_SchedulePersonal); diff --git a/src/pages/NoMatch.tsx b/src/pages/NoMatch.tsx index 3f4726b331..414c9fd0ac 100644 --- a/src/pages/NoMatch.tsx +++ b/src/pages/NoMatch.tsx @@ -1,22 +1,22 @@ import React, { useEffect, useMemo } from 'react'; import qs from 'query-string'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { DEFAULT_PAGE, PLUGIN_ROOT } from 'utils/consts'; import { getPathFromQueryParams } from 'utils/url'; export const NoMatch = () => { - const history = useHistory(); + const navigate = useNavigate(); const query = useMemo(() => qs.parse(window.location.search), [window.location.search]); useEffect(() => { if (query.page) { const path = getPathFromQueryParams(query); - history.push(path); + navigate(path); } else { - history.push(`${PLUGIN_ROOT}/${DEFAULT_PAGE}`); + navigate(`${PLUGIN_ROOT}/${DEFAULT_PAGE}`); } }, [query]); diff --git a/src/pages/escalation-chains/EscalationChains.tsx b/src/pages/escalation-chains/EscalationChains.tsx index 562baba98d..a4df4c1007 100644 --- a/src/pages/escalation-chains/EscalationChains.tsx +++ b/src/pages/escalation-chains/EscalationChains.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, HorizontalGroup, Icon, IconButton, Tooltip, VerticalGroup, withTheme2 } from '@grafana/ui'; import { observer } from 'mobx-react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { getUtilStyles } from 'styles/utils.styles'; import { Collapse } from 'components/Collapse/Collapse'; @@ -30,10 +29,15 @@ import { PageProps, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import { UserActions } from 'utils/authorization/authorization'; import { PAGE, PLUGIN_ROOT } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { getEscalationChainStyles } from './EscalationChains.styles'; -interface EscalationChainsPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> { +interface RouteProps { + id: string; +} + +interface EscalationChainsPageProps extends WithStoreProps, PageProps, PropsWithRouter { theme: GrafanaTheme2; } @@ -60,7 +64,7 @@ class _EscalationChainsPage extends React.Component { - const { history } = this.props; + const { + router: { navigate }, + } = this.props; - history.push(`${PLUGIN_ROOT}/escalations/${id}${window.location.search}`); + navigate(`${PLUGIN_ROOT}/escalations/${id}${window.location.search}`); }; setSelectedEscalationChain = async (escalationChainId: EscalationChain['id']) => { @@ -119,7 +125,9 @@ class _EscalationChainsPage extends React.Component { const { - match: { + router: { params: { id }, }, } = this.props; @@ -272,7 +280,10 @@ class _EscalationChainsPage extends React.Component { - const { store, history } = this.props; + const { + store, + router: { navigate }, + } = this.props; const { selectedEscalationChain } = this.state; const { escalationChainStore } = store; @@ -280,7 +291,7 @@ class _EscalationChainsPage extends React.Component escalationChain.id === selectedEscalationChain)) { const id = searchResult[0]?.id; - history.push(`${PLUGIN_ROOT}/escalations/${id || ''}${window.location.search}`); + navigate(`${PLUGIN_ROOT}/escalations/${id || ''}${window.location.search}`); } }; @@ -400,11 +411,13 @@ class _EscalationChainsPage extends React.Component { const { selectedEscalationChain } = this.state; - const { history } = this.props; + const { + router: { navigate }, + } = this.props; await this.applyFilters(); - history.push(`${PLUGIN_ROOT}/escalations/${id}${window.location.search}`); + navigate(`${PLUGIN_ROOT}/escalations/${id}${window.location.search}`); // because this page wouldn't detect query.id change if (selectedEscalationChain === id) { @@ -444,7 +457,10 @@ class _EscalationChainsPage extends React.Component { - const { store, history } = this.props; + const { + store, + router: { navigate }, + } = this.props; const { escalationChainStore } = store; const { selectedEscalationChain, extraEscalationChains } = this.state; @@ -464,10 +480,9 @@ class _EscalationChainsPage extends React.Component { @@ -480,4 +495,6 @@ class _EscalationChainsPage extends React.Component>( + withMobXProviderContext(withTheme2(_EscalationChainsPage)) +); diff --git a/src/pages/incident/Incident.styles.ts b/src/pages/incident/Incident.styles.ts new file mode 100644 index 0000000000..7afaeb24f9 --- /dev/null +++ b/src/pages/incident/Incident.styles.ts @@ -0,0 +1,220 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Colors, getLabelBackgroundTextColorObject } from 'styles/utils.styles'; + +export const getIncidentStyles = (theme: GrafanaTheme2) => { + return { + incidentRow: css` + display: flex; + `, + + incidentRowLeftSide: css` + flex-grow: 1; + `, + + block: css` + padding: 0 0 20px 0; + `, + + payloadSubtitle: css` + margin-bottom: 16px; + `, + + infoRow: css` + width: 100%; + border-bottom: 1px solid ${theme.colors.border.medium}; + padding-bottom: 20px; + `, + + buttonsRow: css` + margin-top: 20px; + `, + + content: css` + margin-top: 5px; + display: flex; + `, + + timelineIconBackground: css` + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + background: rgba(${theme.isDark ? '70, 76, 84, 1' : '70, 76, 84, 0'}); + `, + + message: css` + margin-top: 16px; + word-wrap: break-word; + + a { + word-break: break-all; + } + + ul { + margin-left: 24px; + } + + p { + margin-bottom: 0; + } + + code { + white-space: break-spaces; + } + `, + + image: css` + margin-top: 16px; + max-width: 100%; + `, + + collapse: css` + margin-top: 16px; + position: relative; + `, + + column: css` + width: 50%; + padding-right: 24px; + + &:not(:first-child) { + padding-left: 24px; + } + `, + + incidentsContent: css` + > div:not(:last-child) { + border-bottom: 1px solid ${Colors.BORDER}; + padding-bottom: 16px; + } + + > div:not(:first-child) { + padding-top: 16px; + } + `, + + timeline: css` + list-style-type: none; + margin: 0 0 24px 12px; + `, + + timelineItem: css` + margin-top: 12px; + `, + + notFound: css` + margin: 50px auto; + text-align: center; + `, + + alertGroupStub: css` + margin: 24px auto; + width: 520px; + text-align: center; + `, + + alertGroupStubDivider: css` + width: 520px; + `, + + blue: css` + background: ${getLabelBackgroundTextColorObject('blue', theme).sourceColor}; + `, + + timelineTitle: css` + margin-bottom: 24px; + `, + + timelineFilter: css` + margin-bottom: 24px; + `, + + titleIcon: css` + color: ${theme.colors.secondary.text}; + margin-left: 4px; + `, + + integrationLogo: css` + margin-right: 8px; + `, + + labelButton: css` + padding: 0 8px; + font-weight: 400; + + &:disabled { + border: 1px solid ${theme.colors.border.strong}; + } + `, + + labelButtonText: css` + max-width: 160px; + overflow: hidden; + position: relative; + display: inline-block; + text-align: center; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + `, + + sourceName: css` + display: flex; + align-items: center; + `, + + statusTagContainer: css` + margin-right: 8px; + display: inherit; + `, + + statusTag: css` + height: 24px; + padding: 5px 8px; + border-radius: 2px; + `, + + pagedUsers: css` + width: 100%; + `, + + // TODO: Where are trash-button/hover-button coming from? + pagedUsersList: css` + list-style-type: none; + margin-bottom: 20px; + width: 100%; + + & > li .trash-button { + display: none; + } + + & > li:hover .trash-button { + display: block; + } + + & > li { + padding: 8px 12px; + width: 100%; + + & .hover-button { + display: none; + } + } + + & > li:hover { + background: ${theme.colors.background.secondary}; + + & .hover-button { + display: inline-flex; + } + } + `, + + userBadge: css` + vertical-align: middle; + `, + }; +}; diff --git a/src/pages/incident/Incident.tsx b/src/pages/incident/Incident.tsx index 5b8ec56463..e4ffd9c28e 100644 --- a/src/pages/incident/Incident.tsx +++ b/src/pages/incident/Incident.tsx @@ -1,6 +1,6 @@ import React, { useState, SyntheticEvent } from 'react'; -import { css, cx } from '@emotion/css'; +import { cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { LabelTag } from '@grafana/labels'; import { @@ -25,9 +25,7 @@ import { observer } from 'mobx-react'; import moment from 'moment-timezone'; import CopyToClipboard from 'react-copy-to-clipboard'; import Emoji from 'react-emoji-render'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import reactStringReplace from 'react-string-replace'; -import { Colors, getLabelBackgroundTextColorObject } from 'styles/utils.styles'; import { OnCallPluginExtensionPoints } from 'types'; import errorSVG from 'assets/img/error.svg'; @@ -64,15 +62,21 @@ import { useStore } from 'state/useStore'; import { withMobXProviderContext } from 'state/withStore'; import { UserActions } from 'utils/authorization/authorization'; import { INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { sanitize } from 'utils/sanitize'; import { parseURL } from 'utils/url'; import { openNotification } from 'utils/utils'; import { getActionButtons } from './Incident.helpers'; +import { getIncidentStyles } from './Incident.styles'; const INTEGRATION_NAME_LENGTH_LIMIT = 30; -interface IncidentPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> { +interface RouteProps { + id: string; +} + +interface IncidentPageProps extends WithStoreProps, PageProps, PropsWithRouter { theme: GrafanaTheme2; } @@ -102,7 +106,7 @@ class _IncidentPage extends React.Component { return async () => { const { - match: { + router: { params: { id: alertId }, }, } = this.props; @@ -289,12 +293,13 @@ class _IncidentPage extends React.Component) => { const { store, - match: { + router: { params: { id: alertId }, }, } = this.props; @@ -518,13 +523,13 @@ class _IncidentPage extends React.Component { const { store, - match: { + router: { params: { id }, }, theme, } = this.props; - const styles = getStyles(theme); + const styles = getIncidentStyles(theme); const incident = store.alertGroupStore.alerts.get(id); if (!incident.render_after_resolve_report_json) { @@ -634,7 +639,7 @@ class _IncidentPage extends React.Component { const { store, - match: { + router: { params: { id }, }, } = this.props; @@ -709,7 +714,8 @@ class _IncidentPage extends React.Component
(undefined); const [isModalOpen, setIsModalOpen] = useState(false); const payloadJSON = isModalOpen ? JSON.stringify(incidentRawResponse.raw_request_data, null, 4) : undefined; - const styles = useStyles2(getStyles); + const styles = useStyles2(getIncidentStyles); return ( <> @@ -845,7 +851,7 @@ function AttachedIncidentsList({ getUnattachClickHandler(pk: string): void; }) { const store = useStore(); - const styles = useStyles2(getStyles); + const styles = useStyles2(getIncidentStyles); const incident = store.alertGroupStore.alerts.get(id); if (!incident.dependent_alert_groups.length) { @@ -881,7 +887,7 @@ function AttachedIncidentsList({ } const AlertGroupStub = ({ buttons }: { buttons: React.ReactNode }) => { - const styles = useStyles2(getStyles); + const styles = useStyles2(getIncidentStyles); return (
@@ -903,221 +909,6 @@ const AlertGroupStub = ({ buttons }: { buttons: React.ReactNode }) => { ); }; -const getStyles = (theme: GrafanaTheme2) => { - return { - incidentRow: css` - display: flex; - `, - - incidentRowLeftSide: css` - flex-grow: 1; - `, - - block: css` - padding: 0 0 20px 0; - `, - - payloadSubtitle: css` - margin-bottom: 16px; - `, - - infoRow: css` - width: 100%; - border-bottom: 1px solid ${theme.colors.border.medium}; - padding-bottom: 20px; - `, - - buttonsRow: css` - margin-top: 20px; - `, - - content: css` - margin-top: 5px; - display: flex; - `, - - timelineIconBackground: css` - width: 28px; - height: 28px; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - background: rgba(${theme.isDark ? '70, 76, 84, 1' : '70, 76, 84, 0'}); - `, - - message: css` - margin-top: 16px; - word-wrap: break-word; - - a { - word-break: break-all; - } - - ul { - margin-left: 24px; - } - - p { - margin-bottom: 0; - } - - code { - white-space: break-spaces; - } - `, - - image: css` - margin-top: 16px; - max-width: 100%; - `, - - collapse: css` - margin-top: 16px; - position: relative; - `, - - column: css` - width: 50%; - padding-right: 24px; - - &:not(:first-child) { - padding-left: 24px; - } - `, - - incidentsContent: css` - > div:not(:last-child) { - border-bottom: 1px solid ${Colors.BORDER}; - padding-bottom: 16px; - } - - > div:not(:first-child) { - padding-top: 16px; - } - `, - - timeline: css` - list-style-type: none; - margin: 0 0 24px 12px; - `, - - timelineItem: css` - margin-top: 12px; - `, - - notFound: css` - margin: 50px auto; - text-align: center; - `, - - alertGroupStub: css` - margin: 24px auto; - width: 520px; - text-align: center; - `, - - alertGroupStubDivider: css` - width: 520px; - `, - - blue: css` - background: ${getLabelBackgroundTextColorObject('blue', theme).sourceColor}; - `, - - timelineTitle: css` - margin-bottom: 24px; - `, - - timelineFilter: css` - margin-bottom: 24px; - `, - - titleIcon: css` - color: ${theme.colors.secondary.text}; - margin-left: 4px; - `, - - integrationLogo: css` - margin-right: 8px; - `, - - labelButton: css` - padding: 0 8px; - font-weight: 400; - - &:disabled { - border: 1px solid ${theme.colors.border.strong}; - } - `, - - labelButtonText: css` - max-width: 160px; - overflow: hidden; - position: relative; - display: inline-block; - text-align: center; - text-decoration: none; - text-overflow: ellipsis; - white-space: nowrap; - `, - - sourceName: css` - display: flex; - align-items: center; - `, - - statusTagContainer: css` - margin-right: 8px; - display: inherit; - `, - - statusTag: css` - height: 24px; - padding: 5px 8px; - border-radius: 2px; - `, - - pagedUsers: css` - width: 100%; - `, - - // TODO: Where are trash-button/hover-button coming from? - pagedUsersList: css` - list-style-type: none; - margin-bottom: 20px; - width: 100%; - - & > li .trash-button { - display: none; - } - - & > li:hover .trash-button { - display: block; - } - - & > li { - padding: 8px 12px; - width: 100%; - - & .hover-button { - display: none; - } - } - - & > li:hover { - background: ${theme.colors.background.secondary}; - - & .hover-button { - display: inline-flex; - } - } - `, - - userBadge: css` - vertical-align: middle; - `, - }; -}; - -export const IncidentPage = withRouter(withMobXProviderContext(withTheme2(_IncidentPage))); +export const IncidentPage = withRouter>( + withMobXProviderContext(withTheme2(_IncidentPage)) +); diff --git a/src/pages/incidents/Incidents.styles.ts b/src/pages/incidents/Incidents.styles.ts new file mode 100644 index 0000000000..01e240da3f --- /dev/null +++ b/src/pages/incidents/Incidents.styles.ts @@ -0,0 +1,116 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; + +export const getIncidentsStyles = (theme: GrafanaTheme2) => { + return { + select: css` + width: 400px; + `, + + rightSideFilters: css` + display: flex; + gap: 8px; + `, + + alertsSelected: css` + white-space: nowrap; + `, + + actionButtons: css` + width: 100%; + justify-content: flex-end; + `, + + filters: css` + margin-bottom: 20px; + `, + + fieldsDropdown: css` + gap: 8px; + display: flex; + margin-left: auto; + align-items: center; + padding-left: 4px; + `, + + aboveIncidentsTable: css` + display: flex; + justify-content: space-between; + align-items: center; + `, + + horizontalScrollTable: css` + table td:global(.rc-table-cell) { + white-space: nowrap; + padding-right: 16px; + } + `, + + bulkActionsContainer: css` + margin: 10px 0 10px 0; + display: flex; + width: 100%; + `, + + bulkActionsList: css` + display: flex; + align-items: center; + gap: 8px; + `, + + otherUsers: css` + color: ${theme.colors.secondary.text}; + `, + + pagination: css` + width: 100%; + margin-top: 20px; + `, + + title: css` + margin-bottom: 24px; + right: 0; + `, + + btnResults: css` + margin-left: 8px; + `, + + /* filter cards */ + + cards: css` + margin-top: 25px; + `, + + row: css` + display: flex; + flex-wrap: wrap; + margin-left: -8px; + margin-right: -8px; + row-gap: 16px; + `, + + loadingPlaceholder: css` + margin-bottom: 0; + text-align: center; + `, + + col: css` + padding-left: 8px; + padding-right: 8px; + display: block; + flex: 0 0 25%; + max-width: 25%; + + @media (max-width: 1200px) { + flex: 0 0 50%; + max-width: 50%; + } + + @media (max-width: 800px) { + flex: 0 0 100%; + max-width: 100%; + } + `, + }; +}; diff --git a/src/pages/incidents/Incidents.tsx b/src/pages/incidents/Incidents.tsx index a48ca8ebb9..aee90c0e49 100644 --- a/src/pages/incidents/Incidents.tsx +++ b/src/pages/incidents/Incidents.tsx @@ -1,6 +1,6 @@ import React, { SyntheticEvent } from 'react'; -import { css, cx } from '@emotion/css'; +import { cx } from '@emotion/css'; import { GrafanaTheme2, durationToMilliseconds, parseDuration, SelectableValue } from '@grafana/data'; import { LabelTag } from '@grafana/labels'; import { @@ -17,7 +17,6 @@ import { capitalize } from 'lodash-es'; import { observer } from 'mobx-react'; import moment from 'moment-timezone'; import Emoji from 'react-emoji-render'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { bem, getUtilStyles } from 'styles/utils.styles'; import { CardButton } from 'components/CardButton/CardButton'; @@ -56,9 +55,11 @@ import { withMobXProviderContext } from 'state/withStore'; import { LocationHelper } from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization/authorization'; import { INCIDENT_HORIZONTAL_SCROLLING_STORAGE, PAGE, PLUGIN_ROOT } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { getItem, setItem } from 'utils/localStorage'; import { TableColumn } from 'utils/types'; +import { getIncidentsStyles } from './Incidents.styles'; import { IncidentDropdown } from './parts/IncidentDropdown'; import { SilenceSelect } from './parts/SilenceSelect'; @@ -67,7 +68,11 @@ interface Pagination { end: number; } -interface IncidentsPageProps extends WithStoreProps, PageProps, RouteComponentProps { +interface RouteProps { + id: string; +} + +interface IncidentsPageProps extends WithStoreProps, PageProps, PropsWithRouter { theme: GrafanaTheme2; } @@ -164,15 +169,14 @@ class _IncidentsPage extends React.Component { - history.push(`${PLUGIN_ROOT}/alert-groups/${id}`); + navigate(`${PLUGIN_ROOT}/alert-groups/${id}`); }} alertReceiveChannelStore={alertReceiveChannelStore} /> @@ -237,7 +241,7 @@ class _IncidentsPage extends React.Component @@ -340,7 +344,7 @@ class _IncidentsPage extends React.Component @@ -490,7 +494,7 @@ class _IncidentsPage extends React.Component 0; const isBulkUpdate = LoaderHelper.isLoading(store.loaderStore, ActionKey.INCIDENTS_BULK_UPDATE); @@ -570,7 +574,7 @@ class _IncidentsPage extends React.Component { const styles = getUtilStyles(this.props.theme); return ( - + #{record.inside_organization_number} @@ -647,7 +651,7 @@ class _IncidentsPage extends React.Component - + @@ -736,7 +740,7 @@ class _IncidentsPage extends React.Component + {date} {time} @@ -783,7 +787,7 @@ class _IncidentsPage extends React.Component + ); @@ -818,7 +822,7 @@ class _IncidentsPage extends React.Component + {matchingLabel} @@ -1052,118 +1056,6 @@ class _IncidentsPage extends React.Component { - return { - select: css` - width: 400px; - `, - - alertsSelected: css` - white-space: nowrap; - `, - - rightSideFilters: css` - display: flex; - gap: 8px; - `, - - actionButtons: css` - width: 100%; - justify-content: flex-end; - `, - - filters: css` - margin-bottom: 20px; - `, - - fieldsDropdown: css` - gap: 8px; - display: flex; - margin-left: auto; - align-items: center; - padding-left: 4px; - `, - - aboveIncidentsTable: css` - display: flex; - justify-content: space-between; - align-items: center; - `, - - horizontalScrollTable: css` - table td:global(.rc-table-cell) { - white-space: nowrap; - padding-right: 16px; - } - `, - - bulkActionsContainer: css` - margin: 10px 0 10px 0; - display: flex; - width: 100%; - `, - - bulkActionsList: css` - display: flex; - align-items: center; - gap: 8px; - `, - - otherUsers: css` - color: ${theme.colors.secondary.text}; - `, - - pagination: css` - width: 100%; - margin-top: 20px; - `, - - title: css` - margin-bottom: 24px; - right: 0; - `, - - btnResults: css` - margin-left: 8px; - `, - - /* filter cards */ - - cards: css` - margin-top: 25px; - `, - - row: css` - display: flex; - flex-wrap: wrap; - margin-left: -8px; - margin-right: -8px; - row-gap: 16px; - `, - - loadingPlaceholder: css` - margin-bottom: 0; - text-align: center; - `, - - col: css` - padding-left: 8px; - padding-right: 8px; - display: block; - flex: 0 0 25%; - max-width: 25%; - - @media (max-width: 1200px) { - flex: 0 0 50%; - max-width: 50%; - } - - @media (max-width: 800px) { - flex: 0 0 100%; - max-width: 100%; - } - `, - }; -}; - -export const IncidentsPage = withRouter(withMobXProviderContext(withTheme2(_IncidentsPage))); +export const IncidentsPage = withRouter>( + withMobXProviderContext(withTheme2(_IncidentsPage)) +); diff --git a/src/pages/integration/Integration.tsx b/src/pages/integration/Integration.tsx index 8d78a98bc0..a7e4f0b741 100644 --- a/src/pages/integration/Integration.tsx +++ b/src/pages/integration/Integration.tsx @@ -1,28 +1,16 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { LabelTag } from '@grafana/labels'; -import { - Button, - HorizontalGroup, - VerticalGroup, - Icon, - LoadingPlaceholder, - IconButton, - ConfirmModal, - Drawer, - Alert, -} from '@grafana/ui'; +import { Button, HorizontalGroup, VerticalGroup, LoadingPlaceholder, IconButton, Drawer, Alert } from '@grafana/ui'; import cn from 'classnames/bind'; import { get } from 'lodash-es'; import { observer } from 'mobx-react'; import moment from 'moment-timezone'; -import CopyToClipboard from 'react-copy-to-clipboard'; import Emoji from 'react-emoji-render'; -import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom'; import { getTemplatesForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config'; import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesForm.config'; -import { HamburgerContextMenu } from 'components/HamburgerContextMenu/HamburgerContextMenu'; import { IntegrationCollapsibleTreeView, IntegrationCollapsibleItem, @@ -30,7 +18,6 @@ import { import { IntegrationContactPoint } from 'components/IntegrationContactPoint/IntegrationContactPoint'; import { IntegrationHowToConnect } from 'components/IntegrationHowToConnect/IntegrationHowToConnect'; import { IntegrationLogoWithTitle } from 'components/IntegrationLogo/IntegrationLogoWithTitle'; -import { IntegrationSendDemoAlertModal } from 'components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal'; import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; import { IntegrationTag } from 'components/Integrations/IntegrationTag'; import { PageErrorHandlingWrapper, PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; @@ -42,14 +29,8 @@ import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge'; import { EditRegexpRouteTemplateModal } from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal'; import { CollapsedIntegrationRouteDisplay } from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay'; import { ExpandedIntegrationRouteDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay'; -import { IntegrationHeartbeatForm } from 'containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm'; import { IntegrationTemplateList } from 'containers/IntegrationContainers/IntegrationTemplatesList'; -import { IntegrationFormContainer } from 'containers/IntegrationForm/IntegrationFormContainer'; -import { IntegrationLabelsForm } from 'containers/IntegrationLabelsForm/IntegrationLabelsForm'; import { IntegrationTemplate } from 'containers/IntegrationTemplate/IntegrationTemplate'; -import { MaintenanceForm } from 'containers/MaintenanceForm/MaintenanceForm'; -import { CompleteServiceNowModal } from 'containers/ServiceNowConfigDrawer/CompleteServiceNowConfigModal'; -import { ServiceNowConfigDrawer } from 'containers/ServiceNowConfigDrawer/ServiceNowConfigDrawer'; import { TeamName } from 'containers/TeamName/TeamName'; import { UserDisplayWithAvatar } from 'containers/UserDisplay/UserDisplayWithAvatar'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; @@ -67,22 +48,30 @@ import { useStore } from 'state/useStore'; import { withMobXProviderContext } from 'state/withStore'; import { LocationHelper } from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization/authorization'; -import { GENERIC_ERROR, INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts'; -import { withDrawer } from 'utils/hoc'; -import { useDrawer } from 'utils/hooks'; +import { INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts'; +import { PropsWithRouter, withDrawer, withRouter } from 'utils/hoc'; import { getItem, setItem } from 'utils/localStorage'; import { sanitize } from 'utils/sanitize'; import { openNotification, openErrorNotification } from 'utils/utils'; +import { IntegrationActions } from './IntegrationActions'; import { OutgoingTab } from './OutgoingTab/OutgoingTab'; const cx = cn.bind(styles); +export type IntegrationDrawerKey = typeof INTEGRATION_SERVICENOW | 'completeConfig'; + +interface RouteProps { + id: string; +} + interface IntegrationProps extends WithDrawerConfig, WithStoreProps, PageProps, - RouteComponentProps<{ id: string }> {} + PropsWithRouter { + theme: GrafanaTheme2; +} interface IntegrationState extends PageBaseState { isLoading: boolean; @@ -137,10 +126,11 @@ class _IntegrationPage extends React.Component { - const { alertReceiveChannelStore } = this.props.store; const { - params: { id }, - } = this.props.match; + store: { alertReceiveChannelStore }, + router: { + params: { id }, + }, + } = this.props; this.setState( { @@ -621,7 +613,7 @@ class _IntegrationPage extends React.Component { const { store: { alertReceiveChannelStore }, - match: { + router: { params: { id }, }, } = this.props; @@ -691,10 +683,12 @@ class _IntegrationPage extends React.Component { - const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store; const { - params: { id }, - } = this.props.match; + store: { alertReceiveChannelStore, escalationPolicyStore }, + router: { + params: { id }, + }, + } = this.props; try { const channelFilter: ChannelFilter = await alertReceiveChannelStore.saveChannelFilter(channelFilterId, { @@ -718,7 +712,7 @@ class _IntegrationPage extends React.Component { const { store, - match: { + router: { params: { id }, }, } = this.props; @@ -758,17 +752,20 @@ class _IntegrationPage extends React.Component { + const { + router: { navigate }, + } = this.props; await AlertReceiveChannelHelper.deleteAlertReceiveChannel(id); - this.props.history.push(`${PLUGIN_ROOT}/integrations/`); + navigate(`${PLUGIN_ROOT}/integrations/`); }; async loadData() { const { store: { alertReceiveChannelStore, msteamsChannelStore, hasFeature }, - match: { + router: { + navigate, params: { id }, }, - history, } = this.props; const promises: Array> = []; @@ -799,7 +796,7 @@ class _IntegrationPage extends React.Component void; - drawerConfig: ReturnType>; -} - -type IntegrationDrawerKey = typeof INTEGRATION_SERVICENOW | 'completeConfig'; - -const IntegrationActions: React.FC = ({ - alertReceiveChannel, - isLegacyIntegration, - changeIsTemplateSettingsOpen, - drawerConfig, -}) => { - const store = useStore(); - const { alertReceiveChannelStore } = store; - - const history = useHistory(); - - const [confirmModal, setConfirmModal] = useState<{ - isOpen: boolean; - title: any; - dismissText: string; - confirmText: string; - body?: React.ReactNode; - description?: string; - confirmationText?: string; - onConfirm: () => void; - }>(undefined); - - const [isCompleteServiceNowConfigOpen, setIsCompleteServiceNowConfigOpen] = useState(false); - const [isIntegrationSettingsOpen, setIsIntegrationSettingsOpen] = useState(false); - const [isLabelsFormOpen, setLabelsFormOpen] = useState(false); - const [isHeartbeatFormOpen, setIsHeartbeatFormOpen] = useState(false); - const [isDemoModalOpen, setIsDemoModalOpen] = useState(false); - const [maintenanceData, setMaintenanceData] = useState<{ - alert_receive_channel_id: ApiSchemas['AlertReceiveChannel']['id']; - }>(undefined); - - const { closeDrawer, openDrawer, getIsDrawerOpened } = drawerConfig; - - const { id } = alertReceiveChannel; - - useEffect(() => { - /* ServiceNow Only */ - openServiceNowCompleteConfigurationDrawer(); - }, []); - - return ( - <> - {confirmModal && ( - setConfirmModal(undefined)} - /> - )} - - {alertReceiveChannel.demo_alert_enabled && ( - setIsDemoModalOpen(false)} - /> - )} - - {getIsDrawerOpened(INTEGRATION_SERVICENOW) && } - - {isCompleteServiceNowConfigOpen && ( - setIsCompleteServiceNowConfigOpen(false)} /> - )} - - {isIntegrationSettingsOpen && ( - setIsIntegrationSettingsOpen(false)} - onSubmit={async () => { - await alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id); - }} - id={alertReceiveChannel['id']} - navigateToAlertGroupLabels={(_id: ApiSchemas['AlertReceiveChannel']['id']) => { - setIsIntegrationSettingsOpen(false); - setLabelsFormOpen(true); - }} - /> - )} - - {isLabelsFormOpen && ( - { - setLabelsFormOpen(false); - }} - onSubmit={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)} - id={alertReceiveChannel['id']} - onOpenIntegrationSettings={() => { - setIsIntegrationSettingsOpen(true); - }} - /> - )} - - {isHeartbeatFormOpen && ( - setIsHeartbeatFormOpen(false)} - /> - )} - - {maintenanceData && ( - alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)} - onHide={() => setMaintenanceData(undefined)} - /> - )} - -
- - - - -
- openDrawer(INTEGRATION_SERVICENOW), - }, - { - onClick: openLabelsForm, - hidden: !store.hasFeature(AppFeature.Labels), - label: 'Alert group labeling', - requiredPermission: UserActions.IntegrationsWrite, - }, - { - onClick: () => setIsHeartbeatFormOpen(true), - hidden: !showHeartbeatSettings(), - label:
Heartbeat Settings
, - requiredPermission: UserActions.IntegrationsWrite, - }, - { - onClick: openStartMaintenance, - hidden: Boolean(alertReceiveChannel.maintenance_till), - label: 'Start Maintenance', - requiredPermission: UserActions.MaintenanceWrite, - }, - { - onClick: changeIsTemplateSettingsOpen, - label: 'Edit Templates', - requiredPermission: UserActions.MaintenanceWrite, - }, - { - onClick: () => { - setConfirmModal({ - isOpen: true, - confirmText: 'Stop', - dismissText: 'Cancel', - onConfirm: onStopMaintenance, - title: 'Stop Maintenance', - body: ( - - Are you sure you want to stop the maintenance for{' '} - ? - - ), - }); - }, - hidden: !alertReceiveChannel.maintenance_till, - label: 'Stop Maintenance', - requiredPermission: UserActions.MaintenanceWrite, - }, - { - onClick: () => - setConfirmModal({ - isOpen: true, - title: 'Migrate Integration?', - body: ( - - - Are you sure you want to migrate ? - - - - - Integration internal behaviour will be changed - - - Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '} - configuration - - - Integration templates will be reset to suit the new payload - - It is needed to adjust routes manually to the new payload - - - ), - onConfirm: onIntegrationMigrate, - dismissText: 'Cancel', - confirmText: 'Migrate', - }), - hidden: !isLegacyIntegration, - label: 'Migrate', - requiredPermission: UserActions.IntegrationsWrite, - }, - { - label: ( - openNotification('Integration ID is copied')} - > -
- - - UID: {alertReceiveChannel.id} - -
-
- ), - }, - { - onClick: () => { - setConfirmModal({ - isOpen: true, - title: 'Delete Integration?', - body: ( - - Are you sure you want to delete ? - - ), - onConfirm: deleteIntegration, - dismissText: 'Cancel', - confirmText: 'Delete', - }); - }, - hidden: !alertReceiveChannel.allow_delete, - label: ( - - - - Delete Integration - - - ), - requiredPermission: UserActions.IntegrationsWrite, - }, - ]} - /> -
-
- - ); - - function openServiceNowCompleteConfigurationDrawer() { - const isServiceNow = getIsBidirectionalIntegration(alertReceiveChannel); - const isConfigured = alertReceiveChannel.additional_settings?.is_configured; - if (isServiceNow && !isConfigured) { - setIsCompleteServiceNowConfigOpen(true); - } - } - - function getMigrationDisplayName() { - const name = alertReceiveChannel.integration.toLowerCase().replace('legacy_', ''); - switch (name) { - case 'grafana_alerting': - return 'Grafana Alerting'; - case 'alertmanager': - default: - return 'AlertManager'; - } - } - - async function onIntegrationMigrate() { - try { - await AlertReceiveChannelHelper.migrateChannel(alertReceiveChannel.id); - setConfirmModal(undefined); - openNotification('Integration has been successfully migrated.'); - await Promise.all([ - alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id), - alertReceiveChannelStore.fetchTemplates(alertReceiveChannel.id), - ]); - } catch (_err) { - openErrorNotification(GENERIC_ERROR); - } - } - - function showHeartbeatSettings() { - return alertReceiveChannel.is_available_for_integration_heartbeat; - } - - async function deleteIntegration() { - try { - await AlertReceiveChannelHelper.deleteAlertReceiveChannel(alertReceiveChannel.id); - history.push(`${PLUGIN_ROOT}/integrations`); - openNotification('Integration has been succesfully deleted.'); - } catch (_err) { - openErrorNotification(GENERIC_ERROR); - } - } - - function openIntegrationSettings() { - setIsIntegrationSettingsOpen(true); - } - - function openLabelsForm() { - setLabelsFormOpen(true); - } - - function openStartMaintenance() { - setMaintenanceData({ alert_receive_channel_id: alertReceiveChannel.id }); - } - - async function onStopMaintenance() { - setConfirmModal(undefined); - - await AlertReceiveChannelHelper.stopMaintenanceMode(id); - - openNotification('Maintenance has been stopped'); - await alertReceiveChannelStore.fetchItemById(id); - } -}; - interface IntegrationHeaderProps { alertReceiveChannelCounter: AlertReceiveChannelCounters; alertReceiveChannel: ApiSchemas['AlertReceiveChannel']; @@ -1292,4 +951,7 @@ const IntegrationHeader: React.FC = ({ } }; -export const IntegrationPage = withRouter(withMobXProviderContext(withDrawer(_IntegrationPage))); +export const IntegrationPage = withRouter< + RouteProps, + Omit +>(withMobXProviderContext(withDrawer(_IntegrationPage))); diff --git a/src/pages/integration/IntegrationActions.tsx b/src/pages/integration/IntegrationActions.tsx new file mode 100644 index 0000000000..ba7bc8ddda --- /dev/null +++ b/src/pages/integration/IntegrationActions.tsx @@ -0,0 +1,368 @@ +import React, { useEffect, useState } from 'react'; + +import { Button, ConfirmModal, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui'; +import cn from 'classnames/bind'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import Emoji from 'react-emoji-render'; +import { useNavigate } from 'react-router-dom-v5-compat'; + +import { HamburgerContextMenu } from 'components/HamburgerContextMenu/HamburgerContextMenu'; +import { IntegrationSendDemoAlertModal } from 'components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal'; +import { Text } from 'components/Text/Text'; +import { IntegrationHeartbeatForm } from 'containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm'; +import { IntegrationFormContainer } from 'containers/IntegrationForm/IntegrationFormContainer'; +import { IntegrationLabelsForm } from 'containers/IntegrationLabelsForm/IntegrationLabelsForm'; +import { MaintenanceForm } from 'containers/MaintenanceForm/MaintenanceForm'; +import { CompleteServiceNowModal } from 'containers/ServiceNowConfigDrawer/CompleteServiceNowConfigModal'; +import { ServiceNowConfigDrawer } from 'containers/ServiceNowConfigDrawer/ServiceNowConfigDrawer'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers'; +import { ApiSchemas } from 'network/oncall-api/api.types'; +import styles from 'pages/integration/Integration.module.scss'; +import { AppFeature } from 'state/features'; +import { useStore } from 'state/useStore'; +import { UserActions } from 'utils/authorization/authorization'; +import { GENERIC_ERROR, INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts'; +import { useDrawer } from 'utils/hooks'; +import { openErrorNotification, openNotification } from 'utils/utils'; + +import { IntegrationDrawerKey } from './Integration'; +import { getIsBidirectionalIntegration } from './Integration.helper'; + +const cx = cn.bind(styles); + +interface IntegrationActionsProps { + isLegacyIntegration: boolean; + alertReceiveChannel: ApiSchemas['AlertReceiveChannel']; + changeIsTemplateSettingsOpen: () => void; + drawerConfig: ReturnType>; +} + +export const IntegrationActions: React.FC = ({ + alertReceiveChannel, + isLegacyIntegration, + changeIsTemplateSettingsOpen, + drawerConfig, +}) => { + const store = useStore(); + const navigate = useNavigate(); + + const { alertReceiveChannelStore } = store; + + const [confirmModal, setConfirmModal] = useState<{ + isOpen: boolean; + title: any; + dismissText: string; + confirmText: string; + body?: React.ReactNode; + description?: string; + confirmationText?: string; + onConfirm: () => void; + }>(undefined); + + const [isCompleteServiceNowConfigOpen, setIsCompleteServiceNowConfigOpen] = useState(false); + const [isIntegrationSettingsOpen, setIsIntegrationSettingsOpen] = useState(false); + const [isLabelsFormOpen, setLabelsFormOpen] = useState(false); + const [isHeartbeatFormOpen, setIsHeartbeatFormOpen] = useState(false); + const [isDemoModalOpen, setIsDemoModalOpen] = useState(false); + const [maintenanceData, setMaintenanceData] = useState<{ + alert_receive_channel_id: ApiSchemas['AlertReceiveChannel']['id']; + }>(undefined); + + const { closeDrawer, openDrawer, getIsDrawerOpened } = drawerConfig; + + const { id } = alertReceiveChannel; + + useEffect(() => { + /* ServiceNow Only */ + openServiceNowCompleteConfigurationDrawer(); + }, []); + + return ( + <> + {confirmModal && ( + setConfirmModal(undefined)} + /> + )} + + {alertReceiveChannel.demo_alert_enabled && ( + setIsDemoModalOpen(false)} + /> + )} + + {getIsDrawerOpened(INTEGRATION_SERVICENOW) && } + + {isCompleteServiceNowConfigOpen && ( + setIsCompleteServiceNowConfigOpen(false)} /> + )} + + {isIntegrationSettingsOpen && ( + setIsIntegrationSettingsOpen(false)} + onSubmit={async () => { + await alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id); + }} + id={alertReceiveChannel['id']} + navigateToAlertGroupLabels={(_id: ApiSchemas['AlertReceiveChannel']['id']) => { + setIsIntegrationSettingsOpen(false); + setLabelsFormOpen(true); + }} + /> + )} + + {isLabelsFormOpen && ( + { + setLabelsFormOpen(false); + }} + onSubmit={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)} + id={alertReceiveChannel['id']} + onOpenIntegrationSettings={() => { + setIsIntegrationSettingsOpen(true); + }} + /> + )} + + {isHeartbeatFormOpen && ( + setIsHeartbeatFormOpen(false)} + /> + )} + + {maintenanceData && ( + alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)} + onHide={() => setMaintenanceData(undefined)} + /> + )} + +
+ + + + +
+ openDrawer(INTEGRATION_SERVICENOW), + }, + { + onClick: openLabelsForm, + hidden: !store.hasFeature(AppFeature.Labels), + label: 'Alert group labeling', + requiredPermission: UserActions.IntegrationsWrite, + }, + { + onClick: () => setIsHeartbeatFormOpen(true), + hidden: !showHeartbeatSettings(), + label:
Heartbeat Settings
, + requiredPermission: UserActions.IntegrationsWrite, + }, + { + onClick: openStartMaintenance, + hidden: Boolean(alertReceiveChannel.maintenance_till), + label: 'Start Maintenance', + requiredPermission: UserActions.MaintenanceWrite, + }, + { + onClick: changeIsTemplateSettingsOpen, + label: 'Edit Templates', + requiredPermission: UserActions.MaintenanceWrite, + }, + { + onClick: () => { + setConfirmModal({ + isOpen: true, + confirmText: 'Stop', + dismissText: 'Cancel', + onConfirm: onStopMaintenance, + title: 'Stop Maintenance', + body: ( + + Are you sure you want to stop the maintenance for{' '} + ? + + ), + }); + }, + hidden: !alertReceiveChannel.maintenance_till, + label: 'Stop Maintenance', + requiredPermission: UserActions.MaintenanceWrite, + }, + { + onClick: () => + setConfirmModal({ + isOpen: true, + title: 'Migrate Integration?', + body: ( + + + Are you sure you want to migrate ? + + + + - Integration internal behaviour will be changed + + - Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '} + configuration + + - Integration templates will be reset to suit the new payload + - It is needed to adjust routes manually to the new payload + + + ), + onConfirm: onIntegrationMigrate, + dismissText: 'Cancel', + confirmText: 'Migrate', + }), + hidden: !isLegacyIntegration, + label: 'Migrate', + requiredPermission: UserActions.IntegrationsWrite, + }, + { + label: ( + openNotification('Integration ID is copied')} + > +
+ + + UID: {alertReceiveChannel.id} + +
+
+ ), + }, + { + onClick: () => { + setConfirmModal({ + isOpen: true, + title: 'Delete Integration?', + body: ( + + Are you sure you want to delete ? + + ), + onConfirm: deleteIntegration, + dismissText: 'Cancel', + confirmText: 'Delete', + }); + }, + hidden: !alertReceiveChannel.allow_delete, + label: ( + + + + Delete Integration + + + ), + requiredPermission: UserActions.IntegrationsWrite, + }, + ]} + /> +
+
+ + ); + + function openServiceNowCompleteConfigurationDrawer() { + const isServiceNow = getIsBidirectionalIntegration(alertReceiveChannel); + const isConfigured = alertReceiveChannel.additional_settings?.is_configured; + if (isServiceNow && !isConfigured) { + setIsCompleteServiceNowConfigOpen(true); + } + } + + function getMigrationDisplayName() { + const name = alertReceiveChannel.integration.toLowerCase().replace('legacy_', ''); + switch (name) { + case 'grafana_alerting': + return 'Grafana Alerting'; + case 'alertmanager': + default: + return 'AlertManager'; + } + } + + async function onIntegrationMigrate() { + try { + await AlertReceiveChannelHelper.migrateChannel(alertReceiveChannel.id); + setConfirmModal(undefined); + openNotification('Integration has been successfully migrated.'); + await Promise.all([ + alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id), + alertReceiveChannelStore.fetchTemplates(alertReceiveChannel.id), + ]); + } catch (_err) { + openErrorNotification(GENERIC_ERROR); + } + } + + function showHeartbeatSettings() { + return alertReceiveChannel.is_available_for_integration_heartbeat; + } + + async function deleteIntegration() { + try { + await AlertReceiveChannelHelper.deleteAlertReceiveChannel(alertReceiveChannel.id); + navigate(`${PLUGIN_ROOT}/integrations`); + openNotification('Integration has been succesfully deleted.'); + } catch (_err) { + openErrorNotification(GENERIC_ERROR); + } + } + + function openIntegrationSettings() { + setIsIntegrationSettingsOpen(true); + } + + function openLabelsForm() { + setLabelsFormOpen(true); + } + + function openStartMaintenance() { + setMaintenanceData({ alert_receive_channel_id: alertReceiveChannel.id }); + } + + async function onStopMaintenance() { + setConfirmModal(undefined); + + await AlertReceiveChannelHelper.stopMaintenanceMode(id); + + openNotification('Maintenance has been stopped'); + await alertReceiveChannelStore.fetchItemById(id); + } +}; diff --git a/src/pages/integration/OutgoingTab/OutgoingTab.hooks.ts b/src/pages/integration/OutgoingTab/OutgoingTab.hooks.ts index a675b746f2..90c61c0698 100644 --- a/src/pages/integration/OutgoingTab/OutgoingTab.hooks.ts +++ b/src/pages/integration/OutgoingTab/OutgoingTab.hooks.ts @@ -1,4 +1,4 @@ -import { useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom-v5-compat'; import { useStore } from 'state/useStore'; import { LocationHelper } from 'utils/LocationHelper'; diff --git a/src/pages/integrations/Integrations.tsx b/src/pages/integrations/Integrations.tsx index 938c5604bf..fed1b4c317 100644 --- a/src/pages/integrations/Integrations.tsx +++ b/src/pages/integrations/Integrations.tsx @@ -16,10 +16,10 @@ import { withTheme2, } from '@grafana/ui'; import { debounce } from 'lodash-es'; +import { runInAction } from 'mobx'; import { observer } from 'mobx-react'; import CopyToClipboard from 'react-copy-to-clipboard'; import Emoji from 'react-emoji-render'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { getUtilStyles } from 'styles/utils.styles'; import { GTable } from 'components/GTable/GTable'; @@ -55,6 +55,7 @@ import { withMobXProviderContext } from 'state/withStore'; import { LocationHelper } from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization/authorization'; import { PAGE, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { openNotification } from 'utils/utils'; import { getIntegrationsStyles } from './Integrations.styles'; @@ -79,6 +80,10 @@ const TABS = [ const FILTERS_DEBOUNCE_MS = 500; +interface RouteProps { + id: string; +} + interface IntegrationsState extends PageBaseState { integrationsFilters: operations['alert_receive_channels_list']['parameters']['query']; alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id'] | 'new'; @@ -96,7 +101,7 @@ interface IntegrationsState extends PageBaseState { activeTab: TabType; } -interface IntegrationsProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> { +interface IntegrationsProps extends WithStoreProps, PageProps, PropsWithRouter { theme: GrafanaTheme2; } @@ -118,7 +123,7 @@ class _IntegrationsPage extends React.Component { const { store, - match: { + router: { params: { id }, }, } = this.props; @@ -698,11 +703,15 @@ class _IntegrationsPage extends React.Component this.invalidateRequestFn(newPage), }); - store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage; + runInAction(() => { + store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage; + }); LocationHelper.update({ p: newPage }, 'partial'); }; debouncedUpdateIntegrations = debounce(this.applyFilters, FILTERS_DEBOUNCE_MS); } -export const IntegrationsPage = withRouter(withMobXProviderContext(withTheme2(_IntegrationsPage))); +export const IntegrationsPage = withRouter>( + withMobXProviderContext(withTheme2(_IntegrationsPage)) +); diff --git a/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx b/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx index 27da0cb093..99447d9899 100644 --- a/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx +++ b/src/pages/outgoing_webhooks/OutgoingWebhooks.tsx @@ -6,7 +6,6 @@ import { Button, ConfirmModal, ConfirmModalProps, HorizontalGroup, Icon, IconBut import { observer } from 'mobx-react'; import { LegacyNavHeading } from 'navbar/LegacyNavHeading'; import CopyToClipboard from 'react-copy-to-clipboard'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { bem, getUtilStyles } from 'styles/utils.styles'; import { GTable } from 'components/GTable/GTable'; @@ -33,11 +32,17 @@ import { PageProps, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import { isUserActionAllowed, UserActions } from 'utils/authorization/authorization'; import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { openErrorNotification, openNotification } from 'utils/utils'; import { WebhookFormActionType } from './OutgoingWebhooks.types'; -interface OutgoingWebhooksProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string; action: string }> { +interface RouteProps { + id: string; + action: string; +} + +interface OutgoingWebhooksProps extends WithStoreProps, PageProps, PropsWithRouter { theme: GrafanaTheme2; } @@ -59,7 +64,7 @@ class OutgoingWebhooks extends React.Component { await this.onDeleteClick(outgoingWebhookId); this.setState({ outgoingWebhookId: undefined, outgoingWebhookAction: undefined }); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks`); + navigate(`${PLUGIN_ROOT}/outgoing_webhooks`); }} /> )} @@ -368,18 +373,22 @@ class OutgoingWebhooks extends React.Component { - const { history } = this.props; + const { + router: { navigate }, + } = this.props; this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.EDIT_SETTINGS }, () => - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`) + navigate(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`) ); }; onCopyClick = (id: ApiSchemas['Webhook']['id']) => { - const { history } = this.props; + const { + router: { navigate }, + } = this.props; this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.COPY }, () => - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/copy/${id}`) + navigate(`${PLUGIN_ROOT}/outgoing_webhooks/copy/${id}`) ); }; @@ -407,19 +416,23 @@ class OutgoingWebhooks extends React.Component { - const { history } = this.props; + const { + router: { navigate }, + } = this.props; this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.VIEW_LAST_RUN }, () => - history.push(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`) + navigate(`${PLUGIN_ROOT}/outgoing_webhooks/last_run/${id}`) ); }; handleOutgoingWebhookFormHide = () => { - const { history } = this.props; + const { + router: { navigate }, + } = this.props; this.setState({ outgoingWebhookId: undefined, outgoingWebhookAction: undefined }); - history.push(`${PLUGIN_ROOT}/outgoing_webhooks`); + navigate(`${PLUGIN_ROOT}/outgoing_webhooks`); }; } @@ -459,4 +472,6 @@ const getStyles = () => { }; }; -export const OutgoingWebhooksPage = withRouter(withMobXProviderContext(withTheme2(OutgoingWebhooks))); +export const OutgoingWebhooksPage = withRouter>( + withMobXProviderContext(withTheme2(OutgoingWebhooks)) +); diff --git a/src/pages/pages.tsx b/src/pages/pages.tsx index a63bed03ef..980497a4c6 100644 --- a/src/pages/pages.tsx +++ b/src/pages/pages.tsx @@ -1,5 +1,5 @@ import { NavModelItem } from '@grafana/data'; -import { matchPath } from 'react-router-dom'; +import { matchPath } from 'react-router-dom-v5-compat'; import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers'; import { AppFeature } from 'state/features'; @@ -173,14 +173,11 @@ export const pages: { [id: string]: PageDefinition } = [ }, {}); export const ROUTES = { - 'alert-groups': ['alert-groups'], - 'alert-group': ['alert-groups/:id'], + 'alert-groups': ['alert-groups', 'alert-groups/:id'], users: ['users', 'users/:id'], - integrations: ['integrations'], - integration: ['integrations/:id'], + integrations: ['integrations', 'integrations/:id'], escalations: ['escalations', 'escalations/:id'], - schedules: ['schedules'], - schedule: ['schedules/:id'], + schedules: ['schedules', 'schedules/:id'], outgoing_webhooks: ['outgoing_webhooks', 'outgoing_webhooks/:id', 'outgoing_webhooks/:action/:id'], settings: ['settings'], 'chat-ops': ['chat-ops'], @@ -194,18 +191,12 @@ export const ROUTES = { incidents: ['incidents'], }; -export const getRoutesForPage = (name: string) => { - return ROUTES[name].map((route) => `${PLUGIN_ROOT}/${route}`); -}; - export function getMatchedPage(url: string) { return Object.keys(ROUTES).find((key) => { - return ROUTES[key].find((route) => - matchPath(url, { - path: `${PLUGIN_ROOT}/${route}`, - exact: true, - strict: false, - }) - ); + return ROUTES[key].find((route: string) => { + const computedRoute = `${PLUGIN_ROOT}/${route}`; + const isMatch = matchPath({ path: computedRoute, end: true }, url); + return isMatch; + }); }); } diff --git a/src/pages/schedule/Schedule.tsx b/src/pages/schedule/Schedule.tsx index 893f35b4f4..a5a17f2317 100644 --- a/src/pages/schedule/Schedule.tsx +++ b/src/pages/schedule/Schedule.tsx @@ -18,7 +18,6 @@ import { } from '@grafana/ui'; import dayjs from 'dayjs'; import { observer } from 'mobx-react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { PageErrorHandlingWrapper } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { PluginLink } from 'components/PluginLink/PluginLink'; @@ -46,11 +45,16 @@ import { withMobXProviderContext } from 'state/withStore'; import { HTML_ID, scrollToElement } from 'utils/DOM'; import { isUserActionAllowed, UserActions } from 'utils/authorization/authorization'; import { PLUGIN_ROOT } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { getCalendarStartDate, getNewCalendarStartDate, getUTCString } from './Schedule.helpers'; import { getScheduleStyles } from './Schedule.styles'; -interface SchedulePageProps extends PageProps, WithStoreProps, RouteComponentProps<{ id: string }> { +interface RouteProps { + id: string; +} + +interface SchedulePageProps extends PageProps, WithStoreProps, PropsWithRouter { theme: GrafanaTheme2; } @@ -75,7 +79,7 @@ interface SchedulePageState { @observer class _SchedulePage extends React.Component { highlightMyShiftsWasToggled = false; - scheduleId = this.props.match.params.id; + scheduleId = this.props.router.params.id; constructor(props: SchedulePageProps) { super(props); @@ -119,7 +123,7 @@ class _SchedulePage extends React.Component { const { store, - match: { + router: { params: { id: scheduleId }, }, } = this.props; @@ -534,7 +538,7 @@ class _SchedulePage extends React.Component { const { store, - match: { + router: { params: { id: scheduleId }, }, } = this.props; @@ -627,14 +631,14 @@ class _SchedulePage extends React.Component { const { store, - match: { + router: { params: { id }, + navigate, }, - history, } = this.props; await store.scheduleStore.delete(id); - history.replace(`${PLUGIN_ROOT}/schedules`); + navigate(`${PLUGIN_ROOT}/schedules`, { replace: true }); }; handleShowShiftSwapForm = (id: ShiftSwap['id'] | 'new', swap?: { swap_start: string; swap_end: string }) => { @@ -645,7 +649,7 @@ class _SchedulePage extends React.Component>( + withMobXProviderContext(withTheme2(_SchedulePage)) +); diff --git a/src/pages/schedules/Schedules.tsx b/src/pages/schedules/Schedules.tsx index 390abaa755..5238a399ca 100644 --- a/src/pages/schedules/Schedules.tsx +++ b/src/pages/schedules/Schedules.tsx @@ -5,7 +5,6 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup, withTheme2 } from '@grafana/ui'; import { observer } from 'mobx-react'; import qs from 'query-string'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { getUtilStyles } from 'styles/utils.styles'; import { Avatar } from 'components/Avatar/Avatar'; @@ -31,10 +30,11 @@ import { withMobXProviderContext } from 'state/withStore'; import { LocationHelper } from 'utils/LocationHelper'; import { UserActions } from 'utils/authorization/authorization'; import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { getSchedulesStyles } from './Schedules.styles'; -interface SchedulesPageProps extends WithStoreProps, RouteComponentProps, PageProps { +interface SchedulesPageProps extends WithStoreProps, PageProps, PropsWithRouter<{}> { theme: GrafanaTheme2; } @@ -160,9 +160,12 @@ class _SchedulesPage extends React.Component { - const { history, query } = this.props; + const { + router: { navigate }, + query, + } = this.props; - history.push(`${PLUGIN_ROOT}/schedules/${data.id}?${qs.stringify(query)}`); + navigate(`${PLUGIN_ROOT}/schedules/${data.id}?${qs.stringify(query)}`); }; handleExpandRow = (expanded: boolean, data: Schedule) => { @@ -203,9 +206,12 @@ class _SchedulesPage extends React.Component { - const { history, query } = this.props; + const { + router: { navigate }, + query, + } = this.props; - return () => history.push(`${PLUGIN_ROOT}/schedules/${scheduleId}?${qs.stringify(query)}`); + return () => navigate(`${PLUGIN_ROOT}/schedules/${scheduleId}?${qs.stringify(query)}`); }; renderType = (value: number) => { @@ -465,4 +471,6 @@ class _SchedulesPage extends React.Component>( + withMobXProviderContext(withTheme2(_SchedulesPage)) +); diff --git a/src/pages/settings/tabs/Cloud/CloudPage.tsx b/src/pages/settings/tabs/Cloud/CloudPage.tsx index c76b94fad3..c47afe4915 100644 --- a/src/pages/settings/tabs/Cloud/CloudPage.tsx +++ b/src/pages/settings/tabs/Cloud/CloudPage.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Button, Field, HorizontalGroup, Icon, Input, LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { Block } from 'components/GBlock/Block'; import { GTable } from 'components/GTable/GTable'; @@ -16,13 +15,14 @@ import { useStore } from 'state/useStore'; import { withMobXProviderContext } from 'state/withStore'; import { UserActions, determineRequiredAuthString } from 'utils/authorization/authorization'; import { PLUGIN_ROOT } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { openErrorNotification } from 'utils/utils'; import styles from './CloudPage.module.css'; const cx = cn.bind(styles); -interface CloudPageProps extends WithStoreProps, RouteComponentProps {} +interface CloudPageProps extends WithStoreProps, PropsWithRouter<{}> {} const ITEMS_PER_PAGE = 50; const _CloudPage = observer((props: CloudPageProps) => { @@ -37,7 +37,9 @@ const _CloudPage = observer((props: CloudPageProps) => { const [_showConfirmationModal, setShowConfirmationModal] = useState(false); const [syncingUsers, setSyncingUsers] = useState(false); - const { history } = props; + const { + router: { navigate }, + } = props; useEffect(() => { (async () => { @@ -124,7 +126,7 @@ const _CloudPage = observer((props: CloudPageProps) => { variant="secondary" size="sm" className={cx('table-button')} - onClick={() => history.push(`${PLUGIN_ROOT}/users/${user.id}`)} + onClick={() => navigate(`${PLUGIN_ROOT}/users/${user.id}`)} > Configure notifications @@ -403,4 +405,4 @@ const _CloudPage = observer((props: CloudPageProps) => { ); }); -export const CloudPage = withRouter(withMobXProviderContext(_CloudPage)); +export const CloudPage = withRouter<{}, PropsWithRouter<{}>>(withMobXProviderContext(_CloudPage)); diff --git a/src/pages/users/Users.tsx b/src/pages/users/Users.tsx index c65105b9e9..c1cbd05166 100644 --- a/src/pages/users/Users.tsx +++ b/src/pages/users/Users.tsx @@ -6,7 +6,6 @@ import { Alert, Button, HorizontalGroup, VerticalGroup, withTheme2 } from '@graf import { debounce } from 'lodash-es'; import { observer } from 'mobx-react'; import { LegacyNavHeading } from 'navbar/LegacyNavHeading'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { Avatar } from 'components/Avatar/Avatar'; import { GTable } from 'components/GTable/GTable'; @@ -29,13 +28,18 @@ import { withMobXProviderContext } from 'state/withStore'; import { LocationHelper } from 'utils/LocationHelper'; import { UserActions, generateMissingPermissionMessage, isUserActionAllowed } from 'utils/authorization/authorization'; import { PAGE, PLUGIN_ROOT } from 'utils/consts'; +import { PropsWithRouter, withRouter } from 'utils/hoc'; import { getUserRowClassNameFn } from './Users.helpers'; import { getUsersStyles } from './Users.styles'; const DEBOUNCE_MS = 1000; -interface UsersProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> { +interface RouteProps { + id: string; +} + +interface UsersProps extends WithStoreProps, PageProps, PropsWithRouter { theme: GrafanaTheme2; } @@ -92,7 +96,7 @@ class Users extends React.Component { }, DEBOUNCE_MS); componentDidUpdate(prevProps: UsersProps) { - if (prevProps.match.params.id !== this.props.match.params.id) { + if (prevProps.router.params.id !== this.props.router.params.id) { this.parseParams(); } } @@ -102,7 +106,7 @@ class Users extends React.Component { const { store, - match: { + router: { params: { id }, }, } = this.props; @@ -127,7 +131,7 @@ class Users extends React.Component { render() { const { userPkToEdit, errorData } = this.state; const { - match: { + router: { params: { id }, }, theme, @@ -437,11 +441,15 @@ class Users extends React.Component { }; handleHideUserSettings = () => { - const { history } = this.props; + const { + router: { navigate }, + } = this.props; this.setState({ userPkToEdit: undefined }); - history.push(`${PLUGIN_ROOT}/users`); + navigate(`${PLUGIN_ROOT}/users`); }; } -export const UsersPage = withRouter(withMobXProviderContext(withTheme2(Users))); +export const UsersPage = withRouter>( + withMobXProviderContext(withTheme2(Users)) +); diff --git a/src/plugin/GrafanaPluginRootPage.tsx b/src/plugin/GrafanaPluginRootPage.tsx index 28ee755656..5c249ceb97 100644 --- a/src/plugin/GrafanaPluginRootPage.tsx +++ b/src/plugin/GrafanaPluginRootPage.tsx @@ -5,7 +5,7 @@ import classnames from 'classnames'; import { observer, Provider } from 'mobx-react'; import { Header } from 'navbar/Header/Header'; import { LegacyNavTabsBar } from 'navbar/LegacyNavTabsBar'; -import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; +import { Navigate, Route, Routes, useLocation } from 'react-router-dom-v5-compat'; import { AppRootProps } from 'types'; import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; @@ -19,7 +19,7 @@ import { Insights } from 'pages/insights/Insights'; import { IntegrationPage } from 'pages/integration/Integration'; import { IntegrationsPage } from 'pages/integrations/Integrations'; import { OutgoingWebhooksPage } from 'pages/outgoing_webhooks/OutgoingWebhooks'; -import { getMatchedPage, getRoutesForPage, pages } from 'pages/pages'; +import { getMatchedPage, pages } from 'pages/pages'; import { SchedulePage } from 'pages/schedule/Schedule'; import { SchedulesPage } from 'pages/schedules/Schedules'; import { SettingsPage } from 'pages/settings/SettingsPage'; @@ -127,81 +127,52 @@ export const Root = observer((props: AppRootProps) => { } - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* Backwards compatibility redirect routes */} - ( - - )} - /> - ( - - )} - /> - - - - - + render={() => ( + + + } /> + } /> + + + + } /> + } /> + + + + } /> + } /> + + + + } /> + } /> + + + + } /> + } /> + + + + } /> + } /> + } /> + + + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + + } /> + + )} + />
diff --git a/src/utils/hoc.tsx b/src/utils/hoc.tsx index 9b743942b4..93b54c752f 100644 --- a/src/utils/hoc.tsx +++ b/src/utils/hoc.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import { NavigateFunction, useLocation, useNavigate, useParams } from 'react-router-dom-v5-compat'; + import { useDrawer } from './hooks'; export const withDrawer = (Component: React.ComponentType) => { @@ -9,3 +11,25 @@ export const withDrawer = (Component: React.ComponentType }; return ComponentWithDrawer; }; + +interface Router { + location: Location; + navigate: NavigateFunction; + params: Readonly; +} + +export interface PropsWithRouter { + router: Router; +} + +export function withRouter>(Component: React.FC): React.FC> { + function HOCWithRouter(props: T) { + const location = useLocation(); + const navigate = useNavigate(); + const params = useParams() as unknown as X; + + return ; + } + + return HOCWithRouter; +} diff --git a/src/utils/hooks.tsx b/src/utils/hooks.tsx index e107c40237..27c920d962 100644 --- a/src/utils/hooks.tsx +++ b/src/utils/hooks.tsx @@ -1,7 +1,7 @@ import React, { ComponentProps, useEffect, useRef, useState } from 'react'; import { ConfirmModal, useStyles2 } from '@grafana/ui'; -import { useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom-v5-compat'; import { ActionKey } from 'models/loader/action-keys'; import { LoaderHelper } from 'models/loader/loader.helpers'; diff --git a/webpack.config.ts b/webpack.config.ts index cccf4a11c2..c4ded77bce 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -86,4 +86,4 @@ const config = async (env): Promise => { })(baseConfig, customConfig); }; -export default config; +export default config; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index f6c6070def..352fc5307e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -486,6 +486,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.7.6": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" + integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.3.3": version "7.18.10" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" @@ -2840,6 +2847,11 @@ pluralize "^8.0.0" yaml-ast-parser "0.0.43" +"@remix-run/router@1.18.0": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.18.0.tgz#20b033d1f542a100c1d57cfd18ecf442d1784732" + integrity sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw== + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -3243,11 +3255,6 @@ dependencies: "@types/node" "*" -"@types/history@^4.7.11": - version "4.7.11" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" - integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== - "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -3484,23 +3491,6 @@ dependencies: "@types/react" "*" -"@types/react-router-dom@^5.3.3": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" - integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*": - version "5.1.19" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.19.tgz#9b404246fba7f91474d7008a3d48c17b6e075ad6" - integrity sha512-Fv/5kb2STAEMT3wHzdKQK2z8xKq38EDIGVrutYLmQVVLe+4orDFquU52hQrULnEHinMKv9FSA6lf9+uNT1ITtA== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-test-renderer@^18.0.5": version "18.0.5" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.5.tgz#b67a6ff37acd93d1b971ec4c838f69d52e772db0" @@ -7932,6 +7922,13 @@ history@4.10.1, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" +history@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b" + integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ== + dependencies: + "@babel/runtime" "^7.7.6" + hoist-non-react-statics@3.3.2, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -12161,6 +12158,15 @@ react-responsive@^8.1.0: prop-types "^15.6.1" shallow-equal "^1.1.0" +react-router-dom-v5-compat@^6.25.1: + version "6.25.1" + resolved "https://registry.yarnpkg.com/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.25.1.tgz#326906a61499e331e721d11ea0cd76610a387a97" + integrity sha512-OKyay/LLp+KP56sLc3hfYpVzs1NvOw/b9zoO91Y82siP1mOI/JD5TnwLrpoXL3j5kj9FmTQBx8HyzIXAjjkptQ== + dependencies: + "@remix-run/router" "1.18.0" + history "^5.3.0" + react-router "6.25.1" + react-router-dom@5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.3.tgz#8779fc28e6691d07afcaf98406d3812fe6f11199" @@ -12190,6 +12196,13 @@ react-router@5.3.3: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-router@6.25.1: + version "6.25.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.25.1.tgz#70b4f1af79954cfcfd23f6ddf5c883e8c904203e" + integrity sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw== + dependencies: + "@remix-run/router" "1.18.0" + react-select-event@^5.1.0: version "5.5.1" resolved "https://registry.yarnpkg.com/react-select-event/-/react-select-event-5.5.1.tgz#d67e04a6a51428b1534b15ecb1b82afbe5edddcb" @@ -13500,7 +13513,16 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13618,7 +13640,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13639,6 +13661,13 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -14984,8 +15013,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15003,6 +15031,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"