Migrate react-router to v6 (#4703)

# What this PR does

- Migrate react-router from v5 to v6

Closes https://github.com/grafana/oncall/issues/4031
This commit is contained in:
Rares Mardare 2024-07-26 13:36:20 +03:00 committed by GitHub
parent be1dd672df
commit 0aa3b1dc33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1105 additions and 969 deletions

View file

@ -14,9 +14,6 @@
{ {
"plugins": ["deprecation"], "plugins": ["deprecation"],
"files": ["src/**/*.{ts,tsx}"], "files": ["src/**/*.{ts,tsx}"],
"rules": {
"deprecation/deprecation": "warn"
},
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
} }

View file

@ -199,7 +199,7 @@ const config = async (env): Promise<Configuration> => {
if (isWSL()) { if (isWSL()) {
baseConfig.watchOptions = { baseConfig.watchOptions = {
poll: 3000, // poll: 3000,
ignored: /node_modules/, ignored: /node_modules/,
}; };
} }

View file

@ -12,7 +12,7 @@ module.exports = {
{ {
files: ['src/**/*.{ts,tsx}'], files: ['src/**/*.{ts,tsx}'],
rules: { rules: {
'deprecation/deprecation': 'off', 'deprecation/deprecation': 'warn',
}, },
parserOptions: { parserOptions: {
project: './tsconfig.json', project: './tsconfig.json',

View file

@ -73,7 +73,6 @@
"@types/react-copy-to-clipboard": "^5.0.4", "@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/react-responsive": "^8.0.5", "@types/react-responsive": "^8.0.5",
"@types/react-router-dom": "^5.3.3",
"@types/react-test-renderer": "^18.0.5", "@types/react-test-renderer": "^18.0.5",
"@types/react-transition-group": "^4.4.5", "@types/react-transition-group": "^4.4.5",
"@types/testing-library__jest-dom": "5.14.8", "@types/testing-library__jest-dom": "5.14.8",
@ -170,7 +169,7 @@
"react-hook-form": "^7.50.1", "react-hook-form": "^7.50.1",
"react-modal": "^3.15.1", "react-modal": "^3.15.1",
"react-responsive": "^8.1.0", "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-sortable-hoc": "^1.11.0",
"react-string-replace": "^0.4.4", "react-string-replace": "^0.4.4",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",

View file

@ -3,7 +3,7 @@ import React, { FC, useCallback, useMemo } from 'react';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; 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 { bem } from 'styles/utils.styles';
import { getPathFromQueryParams } from 'utils/url'; import { getPathFromQueryParams } from 'utils/url';

View file

@ -19,7 +19,7 @@ import {
} from '@grafana/ui'; } from '@grafana/ui';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Controller, useForm, useFormContext, FormProvider } from 'react-hook-form'; 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 { HowTheIntegrationWorks } from 'components/HowTheIntegrationWorks/HowTheIntegrationWorks';
import { PluginLink } from 'components/PluginLink/PluginLink'; import { PluginLink } from 'components/PluginLink/PluginLink';
@ -94,7 +94,7 @@ export const IntegrationForm = observer(
onBackClick, onBackClick,
}: IntegrationFormProps) => { }: IntegrationFormProps) => {
const store = useStore(); const store = useStore();
const history = useHistory(); const navigate = useNavigate();
const styles = useStyles2(getIntegrationFormStyles); const styles = useStyles2(getIntegrationFormStyles);
const isNew = id === 'new'; const isNew = id === 'new';
const { const {
@ -453,7 +453,8 @@ export const IntegrationForm = observer(
async function createNewIntegration(): Promise<void | ApiSchemas['AlertReceiveChannelCreate']> { async function createNewIntegration(): Promise<void | ApiSchemas['AlertReceiveChannelCreate']> {
const response = await alertReceiveChannelStore.create({ data, skipErrorHandling: true }); const response = await alertReceiveChannelStore.create({ data, skipErrorHandling: true });
const pushHistory = (id: ApiSchemas['AlertReceiveChannel']['id']) => const pushHistory = (id: ApiSchemas['AlertReceiveChannel']['id']) =>
history.push(`${PLUGIN_ROOT}/integrations/${id}`); navigate(`${PLUGIN_ROOT}/integrations/${id}`);
if (!response) { if (!response) {
return; return;
} }

View file

@ -2,7 +2,7 @@ import React from 'react';
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; 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 { UserHelper } from 'models/user/user.helpers';
import { ApiSchemas } from 'network/oncall-api/api.types'; import { ApiSchemas } from 'network/oncall-api/api.types';

View file

@ -14,7 +14,7 @@ import {
import cn from 'classnames/bind'; import cn from 'classnames/bind';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { FormProvider, useForm, useFormContext } from 'react-hook-form'; 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 { Text } from 'components/Text/Text';
import { OutgoingWebhookStatus } from 'containers/OutgoingWebhookStatus/OutgoingWebhookStatus'; import { OutgoingWebhookStatus } from 'containers/OutgoingWebhookStatus/OutgoingWebhookStatus';
@ -304,7 +304,7 @@ interface EditWebhookTabsProps {
const EditWebhookTabs = (props: EditWebhookTabsProps) => { const EditWebhookTabs = (props: EditWebhookTabsProps) => {
const { id, data, action, onHide, onUpdate, onDelete, onSubmit, onTemplateEditClick, preset } = props; const { id, data, action, onHide, onUpdate, onDelete, onSubmit, onTemplateEditClick, preset } = props;
const history = useHistory(); const navigate = useNavigate();
const [activeTab, setActiveTab] = useState( const [activeTab, setActiveTab] = useState(
action === WebhookFormActionType.EDIT_SETTINGS ? WebhookTabs.Settings.key : WebhookTabs.LastRun.key action === WebhookFormActionType.EDIT_SETTINGS ? WebhookTabs.Settings.key : WebhookTabs.LastRun.key
@ -323,7 +323,7 @@ const EditWebhookTabs = (props: EditWebhookTabsProps) => {
key={WebhookTabs.Settings.key} key={WebhookTabs.Settings.key}
onChangeTab={() => { onChangeTab={() => {
setActiveTab(WebhookTabs.Settings.key); setActiveTab(WebhookTabs.Settings.key);
history.push(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`); navigate(`${PLUGIN_ROOT}/outgoing_webhooks/edit/${id}`);
}} }}
active={activeTab === WebhookTabs.Settings.key} active={activeTab === WebhookTabs.Settings.key}
label={WebhookTabs.Settings.value} label={WebhookTabs.Settings.value}
@ -333,7 +333,7 @@ const EditWebhookTabs = (props: EditWebhookTabsProps) => {
key={WebhookTabs.LastRun.key} key={WebhookTabs.LastRun.key}
onChangeTab={() => { onChangeTab={() => {
setActiveTab(WebhookTabs.LastRun.key); 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} active={activeTab === WebhookTabs.LastRun.key}
label={WebhookTabs.LastRun.value} label={WebhookTabs.LastRun.value}

View file

@ -2,7 +2,7 @@ import React from 'react';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; 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 { OnCallPluginConfigPageProps } from 'types';
import { PluginState } from 'state/plugin/plugin'; import { PluginState } from 'state/plugin/plugin';
@ -17,7 +17,7 @@ jest.mock('../../../package.json', () => ({
version: 'v1.2.3', version: 'v1.2.3',
})); }));
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom-v5-compat', () => ({
useLocation: jest.fn(() => ({ useLocation: jest.fn(() => ({
search: '', search: '',
})), })),

View file

@ -1,7 +1,7 @@
import React, { FC, useCallback, useEffect, useState } from 'react'; import React, { FC, useCallback, useEffect, useState } from 'react';
import { Button, HorizontalGroup, Label, Legend, LinkButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; 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 { OnCallPluginConfigPageProps } from 'types';
import { PluginState, PluginStatusResponseBase } from 'state/plugin/plugin'; import { PluginState, PluginStatusResponseBase } from 'state/plugin/plugin';

View file

@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Badge, BadgeColor, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui'; import { Badge, BadgeColor, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { observer } from 'mobx-react'; 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 { CSSTransition, TransitionGroup } from 'react-transition-group';
import { Avatar } from 'components/Avatar/Avatar'; import { Avatar } from 'components/Avatar/Avatar';
@ -32,14 +32,16 @@ import { getRotationsStyles } from './Rotations.styles';
import animationStyles from './Rotations.module.css'; import animationStyles from './Rotations.module.css';
interface SchedulePersonalProps extends RouteComponentProps { interface SchedulePersonalProps {
userPk: ApiSchemas['User']['pk']; userPk: ApiSchemas['User']['pk'];
onSlotClick?: (event: Event) => void; onSlotClick?: (event: Event) => void;
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
const _SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotClick, history }) => { const _SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotClick }) => {
const store = useStore(); const store = useStore();
const navigate = useNavigate();
const { timezoneStore, scheduleStore, userStore } = store; const { timezoneStore, scheduleStore, userStore } = store;
const updatePersonalEventsLoading = useIsLoading(ActionKey.UPDATE_PERSONAL_EVENTS); const updatePersonalEventsLoading = useIsLoading(ActionKey.UPDATE_PERSONAL_EVENTS);
@ -77,7 +79,7 @@ const _SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotC
}; };
const openSchedule = (event: Event) => { const openSchedule = (event: Event) => {
history.push(`${PLUGIN_ROOT}/schedules/${event.schedule?.id}`); navigate(`${PLUGIN_ROOT}/schedules/${event.schedule?.id}`);
}; };
const currentTimeX = getCurrentTimeX( const currentTimeX = getCurrentTimeX(
@ -172,4 +174,4 @@ const _SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotC
); );
}); });
export const SchedulePersonal = withRouter(withTheme2(_SchedulePersonal)); export const SchedulePersonal = withTheme2(_SchedulePersonal);

View file

@ -1,22 +1,22 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import qs from 'query-string'; 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 { DEFAULT_PAGE, PLUGIN_ROOT } from 'utils/consts';
import { getPathFromQueryParams } from 'utils/url'; import { getPathFromQueryParams } from 'utils/url';
export const NoMatch = () => { export const NoMatch = () => {
const history = useHistory(); const navigate = useNavigate();
const query = useMemo(() => qs.parse(window.location.search), [window.location.search]); const query = useMemo(() => qs.parse(window.location.search), [window.location.search]);
useEffect(() => { useEffect(() => {
if (query.page) { if (query.page) {
const path = getPathFromQueryParams(query); const path = getPathFromQueryParams(query);
history.push(path); navigate(path);
} else { } else {
history.push(`${PLUGIN_ROOT}/${DEFAULT_PAGE}`); navigate(`${PLUGIN_ROOT}/${DEFAULT_PAGE}`);
} }
}, [query]); }, [query]);

View file

@ -3,7 +3,6 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, HorizontalGroup, Icon, IconButton, Tooltip, VerticalGroup, withTheme2 } from '@grafana/ui'; import { Button, HorizontalGroup, Icon, IconButton, Tooltip, VerticalGroup, withTheme2 } from '@grafana/ui';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { getUtilStyles } from 'styles/utils.styles'; import { getUtilStyles } from 'styles/utils.styles';
import { Collapse } from 'components/Collapse/Collapse'; import { Collapse } from 'components/Collapse/Collapse';
@ -30,10 +29,15 @@ import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore'; import { withMobXProviderContext } from 'state/withStore';
import { UserActions } from 'utils/authorization/authorization'; import { UserActions } from 'utils/authorization/authorization';
import { PAGE, PLUGIN_ROOT } from 'utils/consts'; import { PAGE, PLUGIN_ROOT } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { getEscalationChainStyles } from './EscalationChains.styles'; import { getEscalationChainStyles } from './EscalationChains.styles';
interface EscalationChainsPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> { interface RouteProps {
id: string;
}
interface EscalationChainsPageProps extends WithStoreProps, PageProps, PropsWithRouter<RouteProps> {
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
@ -60,7 +64,7 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -101,9 +105,11 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
}; };
handleEsclalationSelect = (id: EscalationChain['id']) => { handleEsclalationSelect = (id: EscalationChain['id']) => {
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']) => { setSelectedEscalationChain = async (escalationChainId: EscalationChain['id']) => {
@ -119,7 +125,9 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
}; };
componentDidUpdate(prevProps: EscalationChainsPageProps) { componentDidUpdate(prevProps: EscalationChainsPageProps) {
if (this.props.match.params.id !== prevProps.match.params.id) { const { router } = this.props;
if (router.params.id !== prevProps.router.params.id) {
this.parseQueryParams(); this.parseQueryParams();
} }
} }
@ -127,7 +135,7 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
render() { render() {
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
}, },
theme, theme,
@ -256,7 +264,7 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
handleFiltersChange = (filters: FiltersValues, isOnMount = false) => { handleFiltersChange = (filters: FiltersValues, isOnMount = false) => {
const { const {
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -272,7 +280,10 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
}; };
autoSelectEscalationChain = () => { autoSelectEscalationChain = () => {
const { store, history } = this.props; const {
store,
router: { navigate },
} = this.props;
const { selectedEscalationChain } = this.state; const { selectedEscalationChain } = this.state;
const { escalationChainStore } = store; const { escalationChainStore } = store;
@ -280,7 +291,7 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
if (!searchResult.find((escalationChain: EscalationChain) => escalationChain.id === selectedEscalationChain)) { if (!searchResult.find((escalationChain: EscalationChain) => escalationChain.id === selectedEscalationChain)) {
const id = searchResult[0]?.id; 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<EscalationChainsPageProps, E
handleEscalationChainCreate = async (id: EscalationChain['id']) => { handleEscalationChainCreate = async (id: EscalationChain['id']) => {
const { selectedEscalationChain } = this.state; const { selectedEscalationChain } = this.state;
const { history } = this.props; const {
router: { navigate },
} = this.props;
await this.applyFilters(); 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 // because this page wouldn't detect query.id change
if (selectedEscalationChain === id) { if (selectedEscalationChain === id) {
@ -444,7 +457,10 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
}; };
handleDeleteEscalationChain = async () => { handleDeleteEscalationChain = async () => {
const { store, history } = this.props; const {
store,
router: { navigate },
} = this.props;
const { escalationChainStore } = store; const { escalationChainStore } = store;
const { selectedEscalationChain, extraEscalationChains } = this.state; const { selectedEscalationChain, extraEscalationChains } = this.state;
@ -464,10 +480,9 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
} }
const escalationChains = escalationChainStore.getSearchResult(); const escalationChains = escalationChainStore.getSearchResult();
const newSelected = escalationChains[index - 1] || escalationChains[0]; const newSelected = escalationChains[index - 1] || escalationChains[0];
history.push(`${PLUGIN_ROOT}/escalations/${newSelected?.id || ''}${window.location.search}`); navigate(`${PLUGIN_ROOT}/escalations/${newSelected?.id || ''}${window.location.search}`);
}; };
handleEscalationChainNameChange = (value: string) => { handleEscalationChainNameChange = (value: string) => {
@ -480,4 +495,6 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
}; };
} }
export const EscalationChainsPage = withRouter(withMobXProviderContext(withTheme2(_EscalationChainsPage))); export const EscalationChainsPage = withRouter<RouteProps, Omit<EscalationChainsPageProps, 'store' | 'meta' | 'theme'>>(
withMobXProviderContext(withTheme2(_EscalationChainsPage))
);

View file

@ -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;
`,
};
};

View file

@ -1,6 +1,6 @@
import React, { useState, SyntheticEvent } from 'react'; import React, { useState, SyntheticEvent } from 'react';
import { css, cx } from '@emotion/css'; import { cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { LabelTag } from '@grafana/labels'; import { LabelTag } from '@grafana/labels';
import { import {
@ -25,9 +25,7 @@ import { observer } from 'mobx-react';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import Emoji from 'react-emoji-render'; import Emoji from 'react-emoji-render';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import reactStringReplace from 'react-string-replace'; import reactStringReplace from 'react-string-replace';
import { Colors, getLabelBackgroundTextColorObject } from 'styles/utils.styles';
import { OnCallPluginExtensionPoints } from 'types'; import { OnCallPluginExtensionPoints } from 'types';
import errorSVG from 'assets/img/error.svg'; import errorSVG from 'assets/img/error.svg';
@ -64,15 +62,21 @@ import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore'; import { withMobXProviderContext } from 'state/withStore';
import { UserActions } from 'utils/authorization/authorization'; import { UserActions } from 'utils/authorization/authorization';
import { INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts'; import { INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { sanitize } from 'utils/sanitize'; import { sanitize } from 'utils/sanitize';
import { parseURL } from 'utils/url'; import { parseURL } from 'utils/url';
import { openNotification } from 'utils/utils'; import { openNotification } from 'utils/utils';
import { getActionButtons } from './Incident.helpers'; import { getActionButtons } from './Incident.helpers';
import { getIncidentStyles } from './Incident.styles';
const INTEGRATION_NAME_LENGTH_LIMIT = 30; const INTEGRATION_NAME_LENGTH_LIMIT = 30;
interface IncidentPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> { interface RouteProps {
id: string;
}
interface IncidentPageProps extends WithStoreProps, PageProps, PropsWithRouter<RouteProps> {
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
@ -102,7 +106,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
} }
componentDidUpdate(prevProps: IncidentPageProps) { componentDidUpdate(prevProps: IncidentPageProps) {
if (this.props.match.params.id !== prevProps.match.params.id) { if (this.props.router.params.id !== prevProps.router.params.id) {
this.update(); this.update();
} }
} }
@ -112,7 +116,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -128,7 +132,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
const { const {
store, store,
query: { cursor, start, perpage }, query: { cursor, start, perpage },
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -136,7 +140,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
const { errorData, showIntegrationSettings, showAttachIncidentForm, silenceModalData } = this.state; const { errorData, showIntegrationSettings, showAttachIncidentForm, silenceModalData } = this.state;
const { isNotFoundError, isWrongTeamError, isUnknownError } = errorData; const { isNotFoundError, isWrongTeamError, isUnknownError } = errorData;
const { alerts } = store.alertGroupStore; const { alerts } = store.alertGroupStore;
const styles = getStyles(this.props.theme); const styles = getIncidentStyles(this.props.theme);
const incident = alerts.get(id); const incident = alerts.get(id);
@ -274,7 +278,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
handlePagedUserRemove = (userId: ApiSchemas['User']['pk']) => { handlePagedUserRemove = (userId: ApiSchemas['User']['pk']) => {
return async () => { return async () => {
const { const {
match: { router: {
params: { id: alertId }, params: { id: alertId },
}, },
} = this.props; } = this.props;
@ -289,12 +293,13 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
const { const {
store, store,
query, query,
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
const { alerts } = store.alertGroupStore; const { alerts } = store.alertGroupStore;
const styles = getStyles(this.props.theme); const styles = getIncidentStyles(this.props.theme);
const incident = alerts.get(id); const incident = alerts.get(id);
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption( const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
@ -492,7 +497,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
handleAddUserResponder = async (user: Omit<UserResponder, 'type'>) => { handleAddUserResponder = async (user: Omit<UserResponder, 'type'>) => {
const { const {
store, store,
match: { router: {
params: { id: alertId }, params: { id: alertId },
}, },
} = this.props; } = this.props;
@ -518,13 +523,13 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
renderTimeline = () => { renderTimeline = () => {
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
}, },
theme, theme,
} = this.props; } = this.props;
const styles = getStyles(theme); const styles = getIncidentStyles(theme);
const incident = store.alertGroupStore.alerts.get(id); const incident = store.alertGroupStore.alerts.get(id);
if (!incident.render_after_resolve_report_json) { if (!incident.render_after_resolve_report_json) {
@ -634,7 +639,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
handleCreateResolutionNote = async () => { handleCreateResolutionNote = async () => {
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -709,7 +714,8 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
} }
function Incident({ incident }: { incident: ApiSchemas['AlertGroup']; datetimeReference: string }) { function Incident({ incident }: { incident: ApiSchemas['AlertGroup']; datetimeReference: string }) {
const styles = useStyles2(getStyles); const styles = useStyles2(getIncidentStyles);
return ( return (
<div key={incident.pk}> <div key={incident.pk}>
<div <div
@ -733,7 +739,7 @@ function GroupedIncidentsList({
}) { }) {
const store = useStore(); const store = useStore();
const incident = store.alertGroupStore.alerts.get(id); const incident = store.alertGroupStore.alerts.get(id);
const styles = useStyles2(getStyles); const styles = useStyles2(getIncidentStyles);
const alerts = incident.alerts; const alerts = incident.alerts;
if (!alerts) { if (!alerts) {
@ -768,7 +774,7 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
const [incidentRawResponse, setIncidentRawResponse] = useState<{ id: string; raw_request_data: any }>(undefined); const [incidentRawResponse, setIncidentRawResponse] = useState<{ id: string; raw_request_data: any }>(undefined);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const payloadJSON = isModalOpen ? JSON.stringify(incidentRawResponse.raw_request_data, null, 4) : undefined; const payloadJSON = isModalOpen ? JSON.stringify(incidentRawResponse.raw_request_data, null, 4) : undefined;
const styles = useStyles2(getStyles); const styles = useStyles2(getIncidentStyles);
return ( return (
<> <>
@ -845,7 +851,7 @@ function AttachedIncidentsList({
getUnattachClickHandler(pk: string): void; getUnattachClickHandler(pk: string): void;
}) { }) {
const store = useStore(); const store = useStore();
const styles = useStyles2(getStyles); const styles = useStyles2(getIncidentStyles);
const incident = store.alertGroupStore.alerts.get(id); const incident = store.alertGroupStore.alerts.get(id);
if (!incident.dependent_alert_groups.length) { if (!incident.dependent_alert_groups.length) {
@ -881,7 +887,7 @@ function AttachedIncidentsList({
} }
const AlertGroupStub = ({ buttons }: { buttons: React.ReactNode }) => { const AlertGroupStub = ({ buttons }: { buttons: React.ReactNode }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getIncidentStyles);
return ( return (
<div className={styles.alertGroupStub}> <div className={styles.alertGroupStub}>
<VerticalGroup align="center" spacing="md"> <VerticalGroup align="center" spacing="md">
@ -903,221 +909,6 @@ const AlertGroupStub = ({ buttons }: { buttons: React.ReactNode }) => {
); );
}; };
const getStyles = (theme: GrafanaTheme2) => { export const IncidentPage = withRouter<RouteProps, Omit<IncidentPageProps, 'store' | 'meta' | 'theme'>>(
return { withMobXProviderContext(withTheme2(_IncidentPage))
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)));

View file

@ -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%;
}
`,
};
};

View file

@ -1,6 +1,6 @@
import React, { SyntheticEvent } from 'react'; 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 { GrafanaTheme2, durationToMilliseconds, parseDuration, SelectableValue } from '@grafana/data';
import { LabelTag } from '@grafana/labels'; import { LabelTag } from '@grafana/labels';
import { import {
@ -17,7 +17,6 @@ import { capitalize } from 'lodash-es';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import Emoji from 'react-emoji-render'; import Emoji from 'react-emoji-render';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { bem, getUtilStyles } from 'styles/utils.styles'; import { bem, getUtilStyles } from 'styles/utils.styles';
import { CardButton } from 'components/CardButton/CardButton'; import { CardButton } from 'components/CardButton/CardButton';
@ -56,9 +55,11 @@ import { withMobXProviderContext } from 'state/withStore';
import { LocationHelper } from 'utils/LocationHelper'; import { LocationHelper } from 'utils/LocationHelper';
import { UserActions } from 'utils/authorization/authorization'; import { UserActions } from 'utils/authorization/authorization';
import { INCIDENT_HORIZONTAL_SCROLLING_STORAGE, PAGE, PLUGIN_ROOT } from 'utils/consts'; import { INCIDENT_HORIZONTAL_SCROLLING_STORAGE, PAGE, PLUGIN_ROOT } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { getItem, setItem } from 'utils/localStorage'; import { getItem, setItem } from 'utils/localStorage';
import { TableColumn } from 'utils/types'; import { TableColumn } from 'utils/types';
import { getIncidentsStyles } from './Incidents.styles';
import { IncidentDropdown } from './parts/IncidentDropdown'; import { IncidentDropdown } from './parts/IncidentDropdown';
import { SilenceSelect } from './parts/SilenceSelect'; import { SilenceSelect } from './parts/SilenceSelect';
@ -67,7 +68,11 @@ interface Pagination {
end: number; end: number;
} }
interface IncidentsPageProps extends WithStoreProps, PageProps, RouteComponentProps { interface RouteProps {
id: string;
}
interface IncidentsPageProps extends WithStoreProps, PageProps, PropsWithRouter<RouteProps> {
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
@ -164,15 +169,14 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
} }
render() { render() {
const { history } = this.props;
const { refreshInterval, showAddAlertGroupForm } = this.state;
const { const {
theme, theme,
store, store,
store: { alertReceiveChannelStore }, store: { alertReceiveChannelStore },
router: { navigate },
} = this.props; } = this.props;
const styles = getStyles(theme); const { showAddAlertGroupForm, refreshInterval } = this.state;
const styles = getIncidentsStyles(theme);
const isLoading = LoaderHelper.isLoading(store.loaderStore, [ const isLoading = LoaderHelper.isLoading(store.loaderStore, [
ActionKey.FETCH_INCIDENTS, ActionKey.FETCH_INCIDENTS,
@ -217,7 +221,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
this.setState({ showAddAlertGroupForm: false }); this.setState({ showAddAlertGroupForm: false });
}} }}
onCreate={(id: ApiSchemas['AlertGroup']['pk']) => { onCreate={(id: ApiSchemas['AlertGroup']['pk']) => {
history.push(`${PLUGIN_ROOT}/alert-groups/${id}`); navigate(`${PLUGIN_ROOT}/alert-groups/${id}`);
}} }}
alertReceiveChannelStore={alertReceiveChannelStore} alertReceiveChannelStore={alertReceiveChannelStore}
/> />
@ -237,7 +241,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
const { stats } = store.alertGroupStore; const { stats } = store.alertGroupStore;
const status = values.status || []; const status = values.status || [];
const styles = getStyles(theme); const styles = getIncidentsStyles(theme);
return ( return (
<div className={cx(styles.cards, styles.row)}> <div className={cx(styles.cards, styles.row)}>
@ -340,7 +344,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
renderIncidentFilters() { renderIncidentFilters() {
const { query, store, theme } = this.props; const { query, store, theme } = this.props;
const styles = getStyles(theme); const styles = getIncidentsStyles(theme);
return ( return (
<div className={styles.filters}> <div className={styles.filters}>
@ -490,7 +494,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
return null; return null;
} }
const styles = getStyles(theme); const styles = getIncidentsStyles(theme);
const hasSelected = selectedIncidentIds.length > 0; const hasSelected = selectedIncidentIds.length > 0;
const isBulkUpdate = LoaderHelper.isLoading(store.loaderStore, ActionKey.INCIDENTS_BULK_UPDATE); const isBulkUpdate = LoaderHelper.isLoading(store.loaderStore, ActionKey.INCIDENTS_BULK_UPDATE);
@ -570,7 +574,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
const isLoading = const isLoading =
LoaderHelper.isLoading(loaderStore, ActionKey.FETCH_INCIDENTS) || filtersStore.options['incidents'] === undefined; LoaderHelper.isLoading(loaderStore, ActionKey.FETCH_INCIDENTS) || filtersStore.options['incidents'] === undefined;
const styles = getStyles(theme); const styles = getIncidentsStyles(theme);
if (results && !results.length) { if (results && !results.length) {
return ( return (
@ -631,7 +635,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
renderId = (record: ApiSchemas['AlertGroup']) => { renderId = (record: ApiSchemas['AlertGroup']) => {
const styles = getUtilStyles(this.props.theme); const styles = getUtilStyles(this.props.theme);
return ( return (
<TextEllipsisTooltip placement="top" content={`#${record.inside_organization_number}`}> <TextEllipsisTooltip placement="top-start" content={`#${record.inside_organization_number}`}>
<Text type="secondary" className={cx(styles.overflowChild)}> <Text type="secondary" className={cx(styles.overflowChild)}>
#{record.inside_organization_number} #{record.inside_organization_number}
</Text> </Text>
@ -647,7 +651,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
return ( return (
<div> <div>
<TextEllipsisTooltip placement="top" content={record.render_for_web.title}> <TextEllipsisTooltip placement="top-start" content={record.render_for_web.title}>
<Text type="link" size="medium" data-testid="integration-url"> <Text type="link" size="medium" data-testid="integration-url">
<PluginLink <PluginLink
query={{ query={{
@ -692,7 +696,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
return ( return (
<TextEllipsisTooltip <TextEllipsisTooltip
className={cx(utilStyles.flex, utilStyles.flexGapXS)} className={cx(utilStyles.flex, utilStyles.flexGapXS)}
placement="top" placement="top-start"
content={record?.alert_receive_channel?.verbal_name || ''} content={record?.alert_receive_channel?.verbal_name || ''}
> >
<IntegrationLogo integration={integration} scale={0.1} /> <IntegrationLogo integration={integration} scale={0.1} />
@ -736,7 +740,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
} }
return ( return (
<VerticalGroup spacing="none"> <VerticalGroup spacing="none" justify="center">
<Text type="secondary">{date}</Text> <Text type="secondary">{date}</Text>
<Text type="secondary">{time}</Text> <Text type="secondary">{time}</Text>
</VerticalGroup> </VerticalGroup>
@ -783,7 +787,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
const styles = getUtilStyles(theme); const styles = getUtilStyles(theme);
return ( return (
<TextEllipsisTooltip placement="top" content={teams[record.team]?.name}> <TextEllipsisTooltip placement="top-start" content={teams[record.team]?.name}>
<TeamName className={styles.overflowChild} team={teams[record.team]} /> <TeamName className={styles.overflowChild} team={teams[record.team]} />
</TextEllipsisTooltip> </TextEllipsisTooltip>
); );
@ -818,7 +822,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
const utilStyles = getUtilStyles(theme); const utilStyles = getUtilStyles(theme);
return ( return (
<TextEllipsisTooltip placement="top" content={matchingLabel}> <TextEllipsisTooltip placement="top-start" content={matchingLabel}>
<Text type="secondary" className={cx(utilStyles.overflowChild, bem(utilStyles.overflowChild, 'line-1'))}> <Text type="secondary" className={cx(utilStyles.overflowChild, bem(utilStyles.overflowChild, 'line-1'))}>
{matchingLabel} {matchingLabel}
</Text> </Text>
@ -1052,118 +1056,6 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
} }
} }
const getStyles = (theme: GrafanaTheme2) => { export const IncidentsPage = withRouter<RouteProps, Omit<IncidentsPageProps, 'store' | 'meta' | 'theme'>>(
return { withMobXProviderContext(withTheme2(_IncidentsPage))
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)));

View file

@ -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 { LabelTag } from '@grafana/labels';
import { import { Button, HorizontalGroup, VerticalGroup, LoadingPlaceholder, IconButton, Drawer, Alert } from '@grafana/ui';
Button,
HorizontalGroup,
VerticalGroup,
Icon,
LoadingPlaceholder,
IconButton,
ConfirmModal,
Drawer,
Alert,
} from '@grafana/ui';
import cn from 'classnames/bind'; import cn from 'classnames/bind';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import CopyToClipboard from 'react-copy-to-clipboard';
import Emoji from 'react-emoji-render'; import Emoji from 'react-emoji-render';
import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom';
import { getTemplatesForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config'; import { getTemplatesForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesForm.config'; import { TemplateForEdit } from 'components/AlertTemplates/CommonAlertTemplatesForm.config';
import { HamburgerContextMenu } from 'components/HamburgerContextMenu/HamburgerContextMenu';
import { import {
IntegrationCollapsibleTreeView, IntegrationCollapsibleTreeView,
IntegrationCollapsibleItem, IntegrationCollapsibleItem,
@ -30,7 +18,6 @@ import {
import { IntegrationContactPoint } from 'components/IntegrationContactPoint/IntegrationContactPoint'; import { IntegrationContactPoint } from 'components/IntegrationContactPoint/IntegrationContactPoint';
import { IntegrationHowToConnect } from 'components/IntegrationHowToConnect/IntegrationHowToConnect'; import { IntegrationHowToConnect } from 'components/IntegrationHowToConnect/IntegrationHowToConnect';
import { IntegrationLogoWithTitle } from 'components/IntegrationLogo/IntegrationLogoWithTitle'; import { IntegrationLogoWithTitle } from 'components/IntegrationLogo/IntegrationLogoWithTitle';
import { IntegrationSendDemoAlertModal } from 'components/IntegrationSendDemoAlertModal/IntegrationSendDemoAlertModal';
import { IntegrationBlock } from 'components/Integrations/IntegrationBlock'; import { IntegrationBlock } from 'components/Integrations/IntegrationBlock';
import { IntegrationTag } from 'components/Integrations/IntegrationTag'; import { IntegrationTag } from 'components/Integrations/IntegrationTag';
import { PageErrorHandlingWrapper, PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { PageErrorHandlingWrapper, PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
@ -42,14 +29,8 @@ import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
import { EditRegexpRouteTemplateModal } from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal'; import { EditRegexpRouteTemplateModal } from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal';
import { CollapsedIntegrationRouteDisplay } from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay'; import { CollapsedIntegrationRouteDisplay } from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay';
import { ExpandedIntegrationRouteDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay'; import { ExpandedIntegrationRouteDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay';
import { IntegrationHeartbeatForm } from 'containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm';
import { IntegrationTemplateList } from 'containers/IntegrationContainers/IntegrationTemplatesList'; 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 { 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 { TeamName } from 'containers/TeamName/TeamName';
import { UserDisplayWithAvatar } from 'containers/UserDisplay/UserDisplayWithAvatar'; import { UserDisplayWithAvatar } from 'containers/UserDisplay/UserDisplayWithAvatar';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
@ -67,22 +48,30 @@ import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore'; import { withMobXProviderContext } from 'state/withStore';
import { LocationHelper } from 'utils/LocationHelper'; import { LocationHelper } from 'utils/LocationHelper';
import { UserActions } from 'utils/authorization/authorization'; import { UserActions } from 'utils/authorization/authorization';
import { GENERIC_ERROR, INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts'; import { INTEGRATION_SERVICENOW, PLUGIN_ROOT } from 'utils/consts';
import { withDrawer } from 'utils/hoc'; import { PropsWithRouter, withDrawer, withRouter } from 'utils/hoc';
import { useDrawer } from 'utils/hooks';
import { getItem, setItem } from 'utils/localStorage'; import { getItem, setItem } from 'utils/localStorage';
import { sanitize } from 'utils/sanitize'; import { sanitize } from 'utils/sanitize';
import { openNotification, openErrorNotification } from 'utils/utils'; import { openNotification, openErrorNotification } from 'utils/utils';
import { IntegrationActions } from './IntegrationActions';
import { OutgoingTab } from './OutgoingTab/OutgoingTab'; import { OutgoingTab } from './OutgoingTab/OutgoingTab';
const cx = cn.bind(styles); const cx = cn.bind(styles);
export type IntegrationDrawerKey = typeof INTEGRATION_SERVICENOW | 'completeConfig';
interface RouteProps {
id: string;
}
interface IntegrationProps interface IntegrationProps
extends WithDrawerConfig<IntegrationDrawerKey>, extends WithDrawerConfig<IntegrationDrawerKey>,
WithStoreProps, WithStoreProps,
PageProps, PageProps,
RouteComponentProps<{ id: string }> {} PropsWithRouter<RouteProps> {
theme: GrafanaTheme2;
}
interface IntegrationState extends PageBaseState { interface IntegrationState extends PageBaseState {
isLoading: boolean; isLoading: boolean;
@ -137,10 +126,11 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
channelFilterIdForEdit, channelFilterIdForEdit,
isTemplateSettingsOpen, isTemplateSettingsOpen,
} = this.state; } = this.state;
const { const {
store, store,
query, query,
match: { router: {
params: { id }, params: { id },
}, },
drawerConfig, drawerConfig,
@ -583,10 +573,12 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
}; };
handleAddNewRoute = () => { handleAddNewRoute = () => {
const { alertReceiveChannelStore } = this.props.store;
const { const {
params: { id }, store: { alertReceiveChannelStore },
} = this.props.match; router: {
params: { id },
},
} = this.props;
this.setState( this.setState(
{ {
@ -621,7 +613,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
renderRoutesFn = (): IntegrationCollapsibleItem[] => { renderRoutesFn = (): IntegrationCollapsibleItem[] => {
const { const {
store: { alertReceiveChannelStore }, store: { alertReceiveChannelStore },
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -691,10 +683,12 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
channelFilterId: ChannelFilter['id'], channelFilterId: ChannelFilter['id'],
filteringTermType?: number filteringTermType?: number
) => { ) => {
const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store;
const { const {
params: { id }, store: { alertReceiveChannelStore, escalationPolicyStore },
} = this.props.match; router: {
params: { id },
},
} = this.props;
try { try {
const channelFilter: ChannelFilter = await alertReceiveChannelStore.saveChannelFilter(channelFilterId, { const channelFilter: ChannelFilter = await alertReceiveChannelStore.saveChannelFilter(channelFilterId, {
@ -718,7 +712,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
onUpdateTemplatesCallback = async (data) => { onUpdateTemplatesCallback = async (data) => {
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -758,17 +752,20 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
}; };
onRemovalFn = async (id: ApiSchemas['AlertReceiveChannel']['id']) => { onRemovalFn = async (id: ApiSchemas['AlertReceiveChannel']['id']) => {
const {
router: { navigate },
} = this.props;
await AlertReceiveChannelHelper.deleteAlertReceiveChannel(id); await AlertReceiveChannelHelper.deleteAlertReceiveChannel(id);
this.props.history.push(`${PLUGIN_ROOT}/integrations/`); navigate(`${PLUGIN_ROOT}/integrations/`);
}; };
async loadData() { async loadData() {
const { const {
store: { alertReceiveChannelStore, msteamsChannelStore, hasFeature }, store: { alertReceiveChannelStore, msteamsChannelStore, hasFeature },
match: { router: {
navigate,
params: { id }, params: { id },
}, },
history,
} = this.props; } = this.props;
const promises: Array<Promise<void | { [key: string]: { alerts_count: number; alert_groups_count: number } }>> = []; const promises: Array<Promise<void | { [key: string]: { alerts_count: number; alert_groups_count: number } }>> = [];
@ -799,7 +796,7 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
} catch (_err) { } catch (_err) {
if (!alertReceiveChannelStore.items[id]) { if (!alertReceiveChannelStore.items[id]) {
// failed fetching the integration (most likely it's not existent) // failed fetching the integration (most likely it's not existent)
history.push(`${PLUGIN_ROOT}/integrations`); navigate(`${PLUGIN_ROOT}/integrations`);
} }
} finally { } finally {
this.setState({ isLoading: false }); this.setState({ isLoading: false });
@ -818,344 +815,6 @@ class _IntegrationPage extends React.Component<IntegrationProps, IntegrationStat
} }
} }
interface IntegrationActionsProps {
isLegacyIntegration: boolean;
alertReceiveChannel: ApiSchemas['AlertReceiveChannel'];
changeIsTemplateSettingsOpen: () => void;
drawerConfig: ReturnType<typeof useDrawer<IntegrationDrawerKey>>;
}
type IntegrationDrawerKey = typeof INTEGRATION_SERVICENOW | 'completeConfig';
const IntegrationActions: React.FC<IntegrationActionsProps> = ({
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 && (
<ConfirmModal
isOpen={confirmModal.isOpen}
title={confirmModal.title}
confirmText={confirmModal.confirmText}
dismissText="Cancel"
body={confirmModal.body}
description={confirmModal.description}
confirmationText={confirmModal.confirmationText}
onConfirm={confirmModal.onConfirm}
onDismiss={() => setConfirmModal(undefined)}
/>
)}
{alertReceiveChannel.demo_alert_enabled && (
<IntegrationSendDemoAlertModal
alertReceiveChannel={alertReceiveChannel}
isOpen={isDemoModalOpen}
onHideOrCancel={() => setIsDemoModalOpen(false)}
/>
)}
{getIsDrawerOpened(INTEGRATION_SERVICENOW) && <ServiceNowConfigDrawer onHide={closeDrawer} />}
{isCompleteServiceNowConfigOpen && (
<CompleteServiceNowModal onHide={() => setIsCompleteServiceNowConfigOpen(false)} />
)}
{isIntegrationSettingsOpen && (
<IntegrationFormContainer
isTableView={false}
onHide={() => setIsIntegrationSettingsOpen(false)}
onSubmit={async () => {
await alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id);
}}
id={alertReceiveChannel['id']}
navigateToAlertGroupLabels={(_id: ApiSchemas['AlertReceiveChannel']['id']) => {
setIsIntegrationSettingsOpen(false);
setLabelsFormOpen(true);
}}
/>
)}
{isLabelsFormOpen && (
<IntegrationLabelsForm
onHide={() => {
setLabelsFormOpen(false);
}}
onSubmit={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)}
id={alertReceiveChannel['id']}
onOpenIntegrationSettings={() => {
setIsIntegrationSettingsOpen(true);
}}
/>
)}
{isHeartbeatFormOpen && (
<IntegrationHeartbeatForm
alertReceveChannelId={alertReceiveChannel['id']}
onClose={() => setIsHeartbeatFormOpen(false)}
/>
)}
{maintenanceData && (
<MaintenanceForm
initialData={maintenanceData}
onUpdate={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)}
onHide={() => setMaintenanceData(undefined)}
/>
)}
<div className={cx('integration__actions')}>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsTest}>
<Button
variant="secondary"
size="md"
onClick={() => setIsDemoModalOpen(true)}
data-testid="send-demo-alert"
disabled={!alertReceiveChannel.demo_alert_enabled}
tooltip={alertReceiveChannel.demo_alert_enabled ? '' : 'Demo Alerts are not enabled for this integration'}
>
Send demo alert
</Button>
</WithPermissionControlTooltip>
<div data-testid="integration-settings-context-menu-wrapper">
<HamburgerContextMenu
items={[
{
onClick: openIntegrationSettings,
label: 'Integration Settings',
},
{
label: 'ServiceNow configuration',
hidden: !getIsBidirectionalIntegration(alertReceiveChannel),
onClick: () => openDrawer(INTEGRATION_SERVICENOW),
},
{
onClick: openLabelsForm,
hidden: !store.hasFeature(AppFeature.Labels),
label: 'Alert group labeling',
requiredPermission: UserActions.IntegrationsWrite,
},
{
onClick: () => setIsHeartbeatFormOpen(true),
hidden: !showHeartbeatSettings(),
label: <div data-testid="integration-heartbeat-settings">Heartbeat Settings</div>,
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: (
<Text type="primary">
Are you sure you want to stop the maintenance for{' '}
<Emoji text={alertReceiveChannel.verbal_name} /> ?
</Text>
),
});
},
hidden: !alertReceiveChannel.maintenance_till,
label: 'Stop Maintenance',
requiredPermission: UserActions.MaintenanceWrite,
},
{
onClick: () =>
setConfirmModal({
isOpen: true,
title: 'Migrate Integration?',
body: (
<VerticalGroup spacing="lg">
<Text type="primary">
Are you sure you want to migrate <Emoji text={alertReceiveChannel.verbal_name} /> ?
</Text>
<VerticalGroup spacing="xs">
<Text type="secondary">- Integration internal behaviour will be changed</Text>
<Text type="secondary">
- Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '}
configuration
</Text>
<Text type="secondary">- Integration templates will be reset to suit the new payload</Text>
<Text type="secondary">- It is needed to adjust routes manually to the new payload</Text>
</VerticalGroup>
</VerticalGroup>
),
onConfirm: onIntegrationMigrate,
dismissText: 'Cancel',
confirmText: 'Migrate',
}),
hidden: !isLegacyIntegration,
label: 'Migrate',
requiredPermission: UserActions.IntegrationsWrite,
},
{
label: (
<CopyToClipboard
text={alertReceiveChannel.id}
onCopy={() => openNotification('Integration ID is copied')}
>
<div>
<HorizontalGroup spacing={'xs'}>
<Icon name="copy" />
<Text type="primary">UID: {alertReceiveChannel.id}</Text>
</HorizontalGroup>
</div>
</CopyToClipboard>
),
},
{
onClick: () => {
setConfirmModal({
isOpen: true,
title: 'Delete Integration?',
body: (
<Text type="primary">
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} /> ?
</Text>
),
onConfirm: deleteIntegration,
dismissText: 'Cancel',
confirmText: 'Delete',
});
},
hidden: !alertReceiveChannel.allow_delete,
label: (
<Text type="danger">
<HorizontalGroup spacing={'xs'}>
<Icon name="trash-alt" />
<span>Delete Integration</span>
</HorizontalGroup>
</Text>
),
requiredPermission: UserActions.IntegrationsWrite,
},
]}
/>
</div>
</div>
</>
);
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 { interface IntegrationHeaderProps {
alertReceiveChannelCounter: AlertReceiveChannelCounters; alertReceiveChannelCounter: AlertReceiveChannelCounters;
alertReceiveChannel: ApiSchemas['AlertReceiveChannel']; alertReceiveChannel: ApiSchemas['AlertReceiveChannel'];
@ -1292,4 +951,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
} }
}; };
export const IntegrationPage = withRouter(withMobXProviderContext(withDrawer<IntegrationDrawerKey>(_IntegrationPage))); export const IntegrationPage = withRouter<
RouteProps,
Omit<IntegrationProps, 'store' | 'meta' | 'theme' | 'drawerConfig'>
>(withMobXProviderContext(withDrawer<IntegrationDrawerKey>(_IntegrationPage)));

View file

@ -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<typeof useDrawer<IntegrationDrawerKey>>;
}
export const IntegrationActions: React.FC<IntegrationActionsProps> = ({
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 && (
<ConfirmModal
isOpen={confirmModal.isOpen}
title={confirmModal.title}
confirmText={confirmModal.confirmText}
dismissText="Cancel"
body={confirmModal.body}
description={confirmModal.description}
confirmationText={confirmModal.confirmationText}
onConfirm={confirmModal.onConfirm}
onDismiss={() => setConfirmModal(undefined)}
/>
)}
{alertReceiveChannel.demo_alert_enabled && (
<IntegrationSendDemoAlertModal
alertReceiveChannel={alertReceiveChannel}
isOpen={isDemoModalOpen}
onHideOrCancel={() => setIsDemoModalOpen(false)}
/>
)}
{getIsDrawerOpened(INTEGRATION_SERVICENOW) && <ServiceNowConfigDrawer onHide={closeDrawer} />}
{isCompleteServiceNowConfigOpen && (
<CompleteServiceNowModal onHide={() => setIsCompleteServiceNowConfigOpen(false)} />
)}
{isIntegrationSettingsOpen && (
<IntegrationFormContainer
isTableView={false}
onHide={() => setIsIntegrationSettingsOpen(false)}
onSubmit={async () => {
await alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id);
}}
id={alertReceiveChannel['id']}
navigateToAlertGroupLabels={(_id: ApiSchemas['AlertReceiveChannel']['id']) => {
setIsIntegrationSettingsOpen(false);
setLabelsFormOpen(true);
}}
/>
)}
{isLabelsFormOpen && (
<IntegrationLabelsForm
onHide={() => {
setLabelsFormOpen(false);
}}
onSubmit={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)}
id={alertReceiveChannel['id']}
onOpenIntegrationSettings={() => {
setIsIntegrationSettingsOpen(true);
}}
/>
)}
{isHeartbeatFormOpen && (
<IntegrationHeartbeatForm
alertReceveChannelId={alertReceiveChannel['id']}
onClose={() => setIsHeartbeatFormOpen(false)}
/>
)}
{maintenanceData && (
<MaintenanceForm
initialData={maintenanceData}
onUpdate={() => alertReceiveChannelStore.fetchItemById(alertReceiveChannel.id)}
onHide={() => setMaintenanceData(undefined)}
/>
)}
<div className={cx('integration__actions')}>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsTest}>
<Button
variant="secondary"
size="md"
onClick={() => setIsDemoModalOpen(true)}
data-testid="send-demo-alert"
disabled={!alertReceiveChannel.demo_alert_enabled}
tooltip={alertReceiveChannel.demo_alert_enabled ? '' : 'Demo Alerts are not enabled for this integration'}
>
Send demo alert
</Button>
</WithPermissionControlTooltip>
<div data-testid="integration-settings-context-menu-wrapper">
<HamburgerContextMenu
items={[
{
onClick: openIntegrationSettings,
label: 'Integration Settings',
},
{
label: 'ServiceNow configuration',
hidden: !getIsBidirectionalIntegration(alertReceiveChannel),
onClick: () => openDrawer(INTEGRATION_SERVICENOW),
},
{
onClick: openLabelsForm,
hidden: !store.hasFeature(AppFeature.Labels),
label: 'Alert group labeling',
requiredPermission: UserActions.IntegrationsWrite,
},
{
onClick: () => setIsHeartbeatFormOpen(true),
hidden: !showHeartbeatSettings(),
label: <div data-testid="integration-heartbeat-settings">Heartbeat Settings</div>,
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: (
<Text type="primary">
Are you sure you want to stop the maintenance for{' '}
<Emoji text={alertReceiveChannel.verbal_name} /> ?
</Text>
),
});
},
hidden: !alertReceiveChannel.maintenance_till,
label: 'Stop Maintenance',
requiredPermission: UserActions.MaintenanceWrite,
},
{
onClick: () =>
setConfirmModal({
isOpen: true,
title: 'Migrate Integration?',
body: (
<VerticalGroup spacing="lg">
<Text type="primary">
Are you sure you want to migrate <Emoji text={alertReceiveChannel.verbal_name} /> ?
</Text>
<VerticalGroup spacing="xs">
<Text type="secondary">- Integration internal behaviour will be changed</Text>
<Text type="secondary">
- Integration URL will stay the same, so no need to change {getMigrationDisplayName()}{' '}
configuration
</Text>
<Text type="secondary">- Integration templates will be reset to suit the new payload</Text>
<Text type="secondary">- It is needed to adjust routes manually to the new payload</Text>
</VerticalGroup>
</VerticalGroup>
),
onConfirm: onIntegrationMigrate,
dismissText: 'Cancel',
confirmText: 'Migrate',
}),
hidden: !isLegacyIntegration,
label: 'Migrate',
requiredPermission: UserActions.IntegrationsWrite,
},
{
label: (
<CopyToClipboard
text={alertReceiveChannel.id}
onCopy={() => openNotification('Integration ID is copied')}
>
<div>
<HorizontalGroup spacing={'xs'}>
<Icon name="copy" />
<Text type="primary">UID: {alertReceiveChannel.id}</Text>
</HorizontalGroup>
</div>
</CopyToClipboard>
),
},
{
onClick: () => {
setConfirmModal({
isOpen: true,
title: 'Delete Integration?',
body: (
<Text type="primary">
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} /> ?
</Text>
),
onConfirm: deleteIntegration,
dismissText: 'Cancel',
confirmText: 'Delete',
});
},
hidden: !alertReceiveChannel.allow_delete,
label: (
<Text type="danger">
<HorizontalGroup spacing={'xs'}>
<Icon name="trash-alt" />
<span>Delete Integration</span>
</HorizontalGroup>
</Text>
),
requiredPermission: UserActions.IntegrationsWrite,
},
]}
/>
</div>
</div>
</>
);
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);
}
};

View file

@ -1,4 +1,4 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom-v5-compat';
import { useStore } from 'state/useStore'; import { useStore } from 'state/useStore';
import { LocationHelper } from 'utils/LocationHelper'; import { LocationHelper } from 'utils/LocationHelper';

View file

@ -16,10 +16,10 @@ import {
withTheme2, withTheme2,
} from '@grafana/ui'; } from '@grafana/ui';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { runInAction } from 'mobx';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import Emoji from 'react-emoji-render'; import Emoji from 'react-emoji-render';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { getUtilStyles } from 'styles/utils.styles'; import { getUtilStyles } from 'styles/utils.styles';
import { GTable } from 'components/GTable/GTable'; import { GTable } from 'components/GTable/GTable';
@ -55,6 +55,7 @@ import { withMobXProviderContext } from 'state/withStore';
import { LocationHelper } from 'utils/LocationHelper'; import { LocationHelper } from 'utils/LocationHelper';
import { UserActions } from 'utils/authorization/authorization'; import { UserActions } from 'utils/authorization/authorization';
import { PAGE, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import { PAGE, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { openNotification } from 'utils/utils'; import { openNotification } from 'utils/utils';
import { getIntegrationsStyles } from './Integrations.styles'; import { getIntegrationsStyles } from './Integrations.styles';
@ -79,6 +80,10 @@ const TABS = [
const FILTERS_DEBOUNCE_MS = 500; const FILTERS_DEBOUNCE_MS = 500;
interface RouteProps {
id: string;
}
interface IntegrationsState extends PageBaseState { interface IntegrationsState extends PageBaseState {
integrationsFilters: operations['alert_receive_channels_list']['parameters']['query']; integrationsFilters: operations['alert_receive_channels_list']['parameters']['query'];
alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id'] | 'new'; alertReceiveChannelId?: ApiSchemas['AlertReceiveChannel']['id'] | 'new';
@ -96,7 +101,7 @@ interface IntegrationsState extends PageBaseState {
activeTab: TabType; activeTab: TabType;
} }
interface IntegrationsProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> { interface IntegrationsProps extends WithStoreProps, PageProps, PropsWithRouter<RouteProps> {
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
@ -118,7 +123,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
} }
componentDidUpdate(prevProps: IntegrationsProps) { componentDidUpdate(prevProps: IntegrationsProps) {
if (prevProps.match.params.id !== this.props.match.params.id) { if (prevProps.router.params.id !== this.props.router.params.id) {
this.parseQueryParams(); this.parseQueryParams();
} }
if (prevProps.query[TAB_QUERY_PARAM_KEY] !== this.props.query[TAB_QUERY_PARAM_KEY]) { if (prevProps.query[TAB_QUERY_PARAM_KEY] !== this.props.query[TAB_QUERY_PARAM_KEY]) {
@ -133,7 +138,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
parseQueryParams = async () => { parseQueryParams = async () => {
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -698,11 +703,15 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
invalidateFn: () => this.invalidateRequestFn(newPage), invalidateFn: () => this.invalidateRequestFn(newPage),
}); });
store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage; runInAction(() => {
store.filtersStore.currentTablePageNum[PAGE.Integrations] = newPage;
});
LocationHelper.update({ p: newPage }, 'partial'); LocationHelper.update({ p: newPage }, 'partial');
}; };
debouncedUpdateIntegrations = debounce(this.applyFilters, FILTERS_DEBOUNCE_MS); debouncedUpdateIntegrations = debounce(this.applyFilters, FILTERS_DEBOUNCE_MS);
} }
export const IntegrationsPage = withRouter(withMobXProviderContext(withTheme2(_IntegrationsPage))); export const IntegrationsPage = withRouter<RouteProps, Omit<IntegrationsProps, 'store' | 'meta' | 'theme'>>(
withMobXProviderContext(withTheme2(_IntegrationsPage))
);

View file

@ -6,7 +6,6 @@ import { Button, ConfirmModal, ConfirmModalProps, HorizontalGroup, Icon, IconBut
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { LegacyNavHeading } from 'navbar/LegacyNavHeading'; import { LegacyNavHeading } from 'navbar/LegacyNavHeading';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { bem, getUtilStyles } from 'styles/utils.styles'; import { bem, getUtilStyles } from 'styles/utils.styles';
import { GTable } from 'components/GTable/GTable'; import { GTable } from 'components/GTable/GTable';
@ -33,11 +32,17 @@ import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore'; import { withMobXProviderContext } from 'state/withStore';
import { isUserActionAllowed, UserActions } from 'utils/authorization/authorization'; import { isUserActionAllowed, UserActions } from 'utils/authorization/authorization';
import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { openErrorNotification, openNotification } from 'utils/utils'; import { openErrorNotification, openNotification } from 'utils/utils';
import { WebhookFormActionType } from './OutgoingWebhooks.types'; 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<RouteProps> {
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
@ -59,7 +64,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
} }
componentDidUpdate(prevProps: OutgoingWebhooksProps) { componentDidUpdate(prevProps: OutgoingWebhooksProps) {
if (prevProps.match.params.id !== this.props.match.params.id && !this.state.outgoingWebhookAction) { if (prevProps.router.params.id !== this.props.router.params.id && !this.state.outgoingWebhookAction) {
this.parseQueryParams(); this.parseQueryParams();
} }
} }
@ -72,7 +77,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
const { const {
store, store,
match: { router: {
params: { id, action }, params: { id, action },
}, },
} = this.props; } = this.props;
@ -103,8 +108,8 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
render() { render() {
const { const {
store: { outgoingWebhookStore, filtersStore, grafanaTeamStore, hasFeature }, store: { outgoingWebhookStore, filtersStore, grafanaTeamStore, hasFeature },
history, router: {
match: { navigate,
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -232,7 +237,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
onDelete={async () => { onDelete={async () => {
await this.onDeleteClick(outgoingWebhookId); await this.onDeleteClick(outgoingWebhookId);
this.setState({ outgoingWebhookId: undefined, outgoingWebhookAction: undefined }); 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<OutgoingWebhooksProps, OutgoingWe
}; };
onEditClick = (id: ApiSchemas['Webhook']['id']) => { onEditClick = (id: ApiSchemas['Webhook']['id']) => {
const { history } = this.props; const {
router: { navigate },
} = this.props;
this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.EDIT_SETTINGS }, () => 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']) => { onCopyClick = (id: ApiSchemas['Webhook']['id']) => {
const { history } = this.props; const {
router: { navigate },
} = this.props;
this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.COPY }, () => 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<OutgoingWebhooksProps, OutgoingWe
}; };
onLastRunClick = (id: ApiSchemas['Webhook']['id']) => { onLastRunClick = (id: ApiSchemas['Webhook']['id']) => {
const { history } = this.props; const {
router: { navigate },
} = this.props;
this.setState({ outgoingWebhookId: id, outgoingWebhookAction: WebhookFormActionType.VIEW_LAST_RUN }, () => 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 = () => { handleOutgoingWebhookFormHide = () => {
const { history } = this.props; const {
router: { navigate },
} = this.props;
this.setState({ outgoingWebhookId: undefined, outgoingWebhookAction: undefined }); 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<RouteProps, Omit<OutgoingWebhooksProps, 'store' | 'meta' | 'theme'>>(
withMobXProviderContext(withTheme2(OutgoingWebhooks))
);

View file

@ -1,5 +1,5 @@
import { NavModelItem } from '@grafana/data'; 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 { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
import { AppFeature } from 'state/features'; import { AppFeature } from 'state/features';
@ -173,14 +173,11 @@ export const pages: { [id: string]: PageDefinition } = [
}, {}); }, {});
export const ROUTES = { export const ROUTES = {
'alert-groups': ['alert-groups'], 'alert-groups': ['alert-groups', 'alert-groups/:id'],
'alert-group': ['alert-groups/:id'],
users: ['users', 'users/:id'], users: ['users', 'users/:id'],
integrations: ['integrations'], integrations: ['integrations', 'integrations/:id'],
integration: ['integrations/:id'],
escalations: ['escalations', 'escalations/:id'], escalations: ['escalations', 'escalations/:id'],
schedules: ['schedules'], schedules: ['schedules', 'schedules/:id'],
schedule: ['schedules/:id'],
outgoing_webhooks: ['outgoing_webhooks', 'outgoing_webhooks/:id', 'outgoing_webhooks/:action/:id'], outgoing_webhooks: ['outgoing_webhooks', 'outgoing_webhooks/:id', 'outgoing_webhooks/:action/:id'],
settings: ['settings'], settings: ['settings'],
'chat-ops': ['chat-ops'], 'chat-ops': ['chat-ops'],
@ -194,18 +191,12 @@ export const ROUTES = {
incidents: ['incidents'], incidents: ['incidents'],
}; };
export const getRoutesForPage = (name: string) => {
return ROUTES[name].map((route) => `${PLUGIN_ROOT}/${route}`);
};
export function getMatchedPage(url: string) { export function getMatchedPage(url: string) {
return Object.keys(ROUTES).find((key) => { return Object.keys(ROUTES).find((key) => {
return ROUTES[key].find((route) => return ROUTES[key].find((route: string) => {
matchPath(url, { const computedRoute = `${PLUGIN_ROOT}/${route}`;
path: `${PLUGIN_ROOT}/${route}`, const isMatch = matchPath({ path: computedRoute, end: true }, url);
exact: true, return isMatch;
strict: false, });
})
);
}); });
} }

View file

@ -18,7 +18,6 @@ import {
} from '@grafana/ui'; } from '@grafana/ui';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { PageErrorHandlingWrapper } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { PageErrorHandlingWrapper } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import { PluginLink } from 'components/PluginLink/PluginLink'; import { PluginLink } from 'components/PluginLink/PluginLink';
@ -46,11 +45,16 @@ import { withMobXProviderContext } from 'state/withStore';
import { HTML_ID, scrollToElement } from 'utils/DOM'; import { HTML_ID, scrollToElement } from 'utils/DOM';
import { isUserActionAllowed, UserActions } from 'utils/authorization/authorization'; import { isUserActionAllowed, UserActions } from 'utils/authorization/authorization';
import { PLUGIN_ROOT } from 'utils/consts'; import { PLUGIN_ROOT } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { getCalendarStartDate, getNewCalendarStartDate, getUTCString } from './Schedule.helpers'; import { getCalendarStartDate, getNewCalendarStartDate, getUTCString } from './Schedule.helpers';
import { getScheduleStyles } from './Schedule.styles'; import { getScheduleStyles } from './Schedule.styles';
interface SchedulePageProps extends PageProps, WithStoreProps, RouteComponentProps<{ id: string }> { interface RouteProps {
id: string;
}
interface SchedulePageProps extends PageProps, WithStoreProps, PropsWithRouter<RouteProps> {
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
@ -75,7 +79,7 @@ interface SchedulePageState {
@observer @observer
class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState> { class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState> {
highlightMyShiftsWasToggled = false; highlightMyShiftsWasToggled = false;
scheduleId = this.props.match.params.id; scheduleId = this.props.router.params.id;
constructor(props: SchedulePageProps) { constructor(props: SchedulePageProps) {
super(props); super(props);
@ -119,7 +123,7 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
const { const {
store, store,
query, query,
match: { router: {
params: { id: scheduleId }, params: { id: scheduleId },
}, },
theme, theme,
@ -508,7 +512,7 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
update = async () => { update = async () => {
const { const {
store, store,
match: { router: {
params: { id: scheduleId }, params: { id: scheduleId },
}, },
} = this.props; } = this.props;
@ -534,7 +538,7 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
handleNameChange = async (value: string) => { handleNameChange = async (value: string) => {
const { const {
store, store,
match: { router: {
params: { id: scheduleId }, params: { id: scheduleId },
}, },
} = this.props; } = this.props;
@ -627,14 +631,14 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
handleDelete = async () => { handleDelete = async () => {
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
navigate,
}, },
history,
} = this.props; } = this.props;
await store.scheduleStore.delete(id); 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 }) => { handleShowShiftSwapForm = (id: ShiftSwap['id'] | 'new', swap?: { swap_start: string; swap_end: string }) => {
@ -645,7 +649,7 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
userStore: { currentUserPk }, userStore: { currentUserPk },
timezoneStore: { currentDateInSelectedTimezone }, timezoneStore: { currentDateInSelectedTimezone },
}, },
match: { router: {
params: { id: scheduleId }, params: { id: scheduleId },
}, },
} = this.props; } = this.props;
@ -718,4 +722,6 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
}; };
} }
export const SchedulePage = withRouter(withMobXProviderContext(withTheme2(_SchedulePage))); export const SchedulePage = withRouter<RouteProps, Omit<SchedulePageProps, 'store' | 'meta' | 'theme'>>(
withMobXProviderContext(withTheme2(_SchedulePage))
);

View file

@ -5,7 +5,6 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup, withTheme2 } from '@grafana/ui'; import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup, withTheme2 } from '@grafana/ui';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import qs from 'query-string'; import qs from 'query-string';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { getUtilStyles } from 'styles/utils.styles'; import { getUtilStyles } from 'styles/utils.styles';
import { Avatar } from 'components/Avatar/Avatar'; import { Avatar } from 'components/Avatar/Avatar';
@ -31,10 +30,11 @@ import { withMobXProviderContext } from 'state/withStore';
import { LocationHelper } from 'utils/LocationHelper'; import { LocationHelper } from 'utils/LocationHelper';
import { UserActions } from 'utils/authorization/authorization'; import { UserActions } from 'utils/authorization/authorization';
import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts'; import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { getSchedulesStyles } from './Schedules.styles'; import { getSchedulesStyles } from './Schedules.styles';
interface SchedulesPageProps extends WithStoreProps, RouteComponentProps, PageProps { interface SchedulesPageProps extends WithStoreProps, PageProps, PropsWithRouter<{}> {
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
@ -160,9 +160,12 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
}; };
handleCreateSchedule = (data: Schedule) => { handleCreateSchedule = (data: Schedule) => {
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) => { handleExpandRow = (expanded: boolean, data: Schedule) => {
@ -203,9 +206,12 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
}; };
getScheduleClickHandler = (scheduleId: Schedule['id']) => { getScheduleClickHandler = (scheduleId: Schedule['id']) => {
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) => { renderType = (value: number) => {
@ -465,4 +471,6 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
}; };
} }
export const SchedulesPage = withRouter(withMobXProviderContext(withTheme2(_SchedulesPage))); export const SchedulesPage = withRouter<{}, Omit<SchedulesPageProps, 'store' | 'meta' | 'theme'>>(
withMobXProviderContext(withTheme2(_SchedulesPage))
);

View file

@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Button, Field, HorizontalGroup, Icon, Input, LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; import { Button, Field, HorizontalGroup, Icon, Input, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind'; import cn from 'classnames/bind';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { Block } from 'components/GBlock/Block'; import { Block } from 'components/GBlock/Block';
import { GTable } from 'components/GTable/GTable'; import { GTable } from 'components/GTable/GTable';
@ -16,13 +15,14 @@ import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore'; import { withMobXProviderContext } from 'state/withStore';
import { UserActions, determineRequiredAuthString } from 'utils/authorization/authorization'; import { UserActions, determineRequiredAuthString } from 'utils/authorization/authorization';
import { PLUGIN_ROOT } from 'utils/consts'; import { PLUGIN_ROOT } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { openErrorNotification } from 'utils/utils'; import { openErrorNotification } from 'utils/utils';
import styles from './CloudPage.module.css'; import styles from './CloudPage.module.css';
const cx = cn.bind(styles); const cx = cn.bind(styles);
interface CloudPageProps extends WithStoreProps, RouteComponentProps {} interface CloudPageProps extends WithStoreProps, PropsWithRouter<{}> {}
const ITEMS_PER_PAGE = 50; const ITEMS_PER_PAGE = 50;
const _CloudPage = observer((props: CloudPageProps) => { const _CloudPage = observer((props: CloudPageProps) => {
@ -37,7 +37,9 @@ const _CloudPage = observer((props: CloudPageProps) => {
const [_showConfirmationModal, setShowConfirmationModal] = useState<boolean>(false); const [_showConfirmationModal, setShowConfirmationModal] = useState<boolean>(false);
const [syncingUsers, setSyncingUsers] = useState<boolean>(false); const [syncingUsers, setSyncingUsers] = useState<boolean>(false);
const { history } = props; const {
router: { navigate },
} = props;
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -124,7 +126,7 @@ const _CloudPage = observer((props: CloudPageProps) => {
variant="secondary" variant="secondary"
size="sm" size="sm"
className={cx('table-button')} className={cx('table-button')}
onClick={() => history.push(`${PLUGIN_ROOT}/users/${user.id}`)} onClick={() => navigate(`${PLUGIN_ROOT}/users/${user.id}`)}
> >
Configure notifications Configure notifications
</Button> </Button>
@ -403,4 +405,4 @@ const _CloudPage = observer((props: CloudPageProps) => {
); );
}); });
export const CloudPage = withRouter(withMobXProviderContext(_CloudPage)); export const CloudPage = withRouter<{}, PropsWithRouter<{}>>(withMobXProviderContext(_CloudPage));

View file

@ -6,7 +6,6 @@ import { Alert, Button, HorizontalGroup, VerticalGroup, withTheme2 } from '@graf
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { LegacyNavHeading } from 'navbar/LegacyNavHeading'; import { LegacyNavHeading } from 'navbar/LegacyNavHeading';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { Avatar } from 'components/Avatar/Avatar'; import { Avatar } from 'components/Avatar/Avatar';
import { GTable } from 'components/GTable/GTable'; import { GTable } from 'components/GTable/GTable';
@ -29,13 +28,18 @@ import { withMobXProviderContext } from 'state/withStore';
import { LocationHelper } from 'utils/LocationHelper'; import { LocationHelper } from 'utils/LocationHelper';
import { UserActions, generateMissingPermissionMessage, isUserActionAllowed } from 'utils/authorization/authorization'; import { UserActions, generateMissingPermissionMessage, isUserActionAllowed } from 'utils/authorization/authorization';
import { PAGE, PLUGIN_ROOT } from 'utils/consts'; import { PAGE, PLUGIN_ROOT } from 'utils/consts';
import { PropsWithRouter, withRouter } from 'utils/hoc';
import { getUserRowClassNameFn } from './Users.helpers'; import { getUserRowClassNameFn } from './Users.helpers';
import { getUsersStyles } from './Users.styles'; import { getUsersStyles } from './Users.styles';
const DEBOUNCE_MS = 1000; const DEBOUNCE_MS = 1000;
interface UsersProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> { interface RouteProps {
id: string;
}
interface UsersProps extends WithStoreProps, PageProps, PropsWithRouter<RouteProps> {
theme: GrafanaTheme2; theme: GrafanaTheme2;
} }
@ -92,7 +96,7 @@ class Users extends React.Component<UsersProps, UsersState> {
}, DEBOUNCE_MS); }, DEBOUNCE_MS);
componentDidUpdate(prevProps: UsersProps) { 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(); this.parseParams();
} }
} }
@ -102,7 +106,7 @@ class Users extends React.Component<UsersProps, UsersState> {
const { const {
store, store,
match: { router: {
params: { id }, params: { id },
}, },
} = this.props; } = this.props;
@ -127,7 +131,7 @@ class Users extends React.Component<UsersProps, UsersState> {
render() { render() {
const { userPkToEdit, errorData } = this.state; const { userPkToEdit, errorData } = this.state;
const { const {
match: { router: {
params: { id }, params: { id },
}, },
theme, theme,
@ -437,11 +441,15 @@ class Users extends React.Component<UsersProps, UsersState> {
}; };
handleHideUserSettings = () => { handleHideUserSettings = () => {
const { history } = this.props; const {
router: { navigate },
} = this.props;
this.setState({ userPkToEdit: undefined }); this.setState({ userPkToEdit: undefined });
history.push(`${PLUGIN_ROOT}/users`); navigate(`${PLUGIN_ROOT}/users`);
}; };
} }
export const UsersPage = withRouter(withMobXProviderContext(withTheme2(Users))); export const UsersPage = withRouter<RouteProps, Omit<UsersProps, 'store' | 'meta' | 'theme'>>(
withMobXProviderContext(withTheme2(Users))
);

View file

@ -5,7 +5,7 @@ import classnames from 'classnames';
import { observer, Provider } from 'mobx-react'; import { observer, Provider } from 'mobx-react';
import { Header } from 'navbar/Header/Header'; import { Header } from 'navbar/Header/Header';
import { LegacyNavTabsBar } from 'navbar/LegacyNavTabsBar'; 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 { AppRootProps } from 'types';
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
@ -19,7 +19,7 @@ import { Insights } from 'pages/insights/Insights';
import { IntegrationPage } from 'pages/integration/Integration'; import { IntegrationPage } from 'pages/integration/Integration';
import { IntegrationsPage } from 'pages/integrations/Integrations'; import { IntegrationsPage } from 'pages/integrations/Integrations';
import { OutgoingWebhooksPage } from 'pages/outgoing_webhooks/OutgoingWebhooks'; 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 { SchedulePage } from 'pages/schedule/Schedule';
import { SchedulesPage } from 'pages/schedules/Schedules'; import { SchedulesPage } from 'pages/schedules/Schedules';
import { SettingsPage } from 'pages/settings/SettingsPage'; import { SettingsPage } from 'pages/settings/SettingsPage';
@ -127,81 +127,52 @@ export const Root = observer((props: AppRootProps) => {
<RenderConditionally <RenderConditionally
shouldRender={isBasicDataLoaded} shouldRender={isBasicDataLoaded}
backupChildren={<LoadingPlaceholder text="Loading..." />} backupChildren={<LoadingPlaceholder text="Loading..." />}
> render={() => (
<Switch> <Routes>
<Route path={getRoutesForPage('alert-groups')} exact> <Route path="alert-groups">
<IncidentsPage query={query} /> <Route path=":id" element={<IncidentPage query={query} />} />
</Route> <Route index element={<IncidentsPage query={query} />} />
<Route path={getRoutesForPage('alert-group')} exact> </Route>
<IncidentPage query={query} />
</Route>
<Route path={getRoutesForPage('users')} exact>
<UsersPage query={query} />
</Route>
<Route path={getRoutesForPage('integrations')} exact>
<IntegrationsPage query={query} />
</Route>
<Route path={getRoutesForPage('integration')} exact>
<IntegrationPage query={query} />
</Route>
<Route path={getRoutesForPage('escalations')} exact>
<EscalationChainsPage query={query} />
</Route>
<Route path={getRoutesForPage('schedules')} exact>
<SchedulesPage query={query} />
</Route>
<Route path={getRoutesForPage('schedule')} exact>
<SchedulePage query={query} />
</Route>
<Route path={getRoutesForPage('outgoing_webhooks')} exact>
<OutgoingWebhooksPage query={query} />
</Route>
<Route path={getRoutesForPage('settings')} exact>
<SettingsPage />
</Route>
<Route path={getRoutesForPage('chat-ops')} exact>
<ChatOpsPage query={query} />
</Route>
<Route path={getRoutesForPage('live-settings')} exact>
<LiveSettings />
</Route>
<Route path={getRoutesForPage('cloud')} exact>
<CloudPage />
</Route>
<Route path={getRoutesForPage('insights')} exact>
<Insights />
</Route>
{/* Backwards compatibility redirect routes */} <Route path="users">
<Route <Route path=":id" element={<UsersPage query={query} />} />
path={getRoutesForPage('incident')} <Route index element={<UsersPage query={query} />} />
exact </Route>
render={({ location }) => (
<Redirect <Route path="integrations">
to={{ <Route path=":id" element={<IntegrationPage query={query} />} />
...location, <Route index element={<IntegrationsPage query={query} />} />
pathname: location.pathname.replace(/incident/, 'alert-group'), </Route>
}}
></Redirect> <Route path="escalations">
)} <Route path=":id" element={<EscalationChainsPage query={query} />} />
/> <Route index element={<EscalationChainsPage query={query} />} />
<Route </Route>
path={getRoutesForPage('incidents')}
exact <Route path="schedules">
render={({ location }) => ( <Route path=":id" element={<SchedulePage query={query} />} />
<Redirect <Route index element={<SchedulesPage query={query} />} />
to={{ </Route>
...location,
pathname: location.pathname.replace(/incidents/, 'alert-groups'), <Route path="outgoing_webhooks">
}} <Route path=":action/:id" element={<OutgoingWebhooksPage query={query} />} />
></Redirect> <Route path=":id" element={<OutgoingWebhooksPage query={query} />} />
)} <Route index element={<OutgoingWebhooksPage query={query} />} />
/> </Route>
<Route path="*">
<NoMatch /> <Route path="settings" element={<SettingsPage />} />
</Route> <Route path="chat-ops" element={<ChatOpsPage query={query} />} />
</Switch> <Route path="live-settings" element={<LiveSettings />} />
</RenderConditionally> <Route path="cloud" element={<CloudPage />} />
<Route path="insights" element={<Insights />} />
<Route path="incident" element={<Navigate to="alert-group" replace />} />
<Route path="incidents" element={<Navigate to="alert-groups" replace />} />
<Route path="*" element={<NoMatch />} />
</Routes>
)}
/>
</RenderConditionally> </RenderConditionally>
</div> </div>
</DefaultPageLayout> </DefaultPageLayout>

View file

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { NavigateFunction, useLocation, useNavigate, useParams } from 'react-router-dom-v5-compat';
import { useDrawer } from './hooks'; import { useDrawer } from './hooks';
export const withDrawer = <T extends string>(Component: React.ComponentType<any>) => { export const withDrawer = <T extends string>(Component: React.ComponentType<any>) => {
@ -9,3 +11,25 @@ export const withDrawer = <T extends string>(Component: React.ComponentType<any>
}; };
return ComponentWithDrawer; return ComponentWithDrawer;
}; };
interface Router<T> {
location: Location;
navigate: NavigateFunction;
params: Readonly<T>;
}
export interface PropsWithRouter<T> {
router: Router<T>;
}
export function withRouter<X, T extends PropsWithRouter<X>>(Component: React.FC<T>): React.FC<Omit<T, 'router'>> {
function HOCWithRouter(props: T) {
const location = useLocation();
const navigate = useNavigate();
const params = useParams() as unknown as X;
return <Component {...props} router={{ location, navigate, params }} />;
}
return HOCWithRouter;
}

View file

@ -1,7 +1,7 @@
import React, { ComponentProps, useEffect, useRef, useState } from 'react'; import React, { ComponentProps, useEffect, useRef, useState } from 'react';
import { ConfirmModal, useStyles2 } from '@grafana/ui'; 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 { ActionKey } from 'models/loader/action-keys';
import { LoaderHelper } from 'models/loader/loader.helpers'; import { LoaderHelper } from 'models/loader/loader.helpers';

View file

@ -86,4 +86,4 @@ const config = async (env): Promise<Configuration> => {
})(baseConfig, customConfig); })(baseConfig, customConfig);
}; };
export default config; export default config;

View file

@ -486,6 +486,13 @@
dependencies: dependencies:
regenerator-runtime "^0.14.0" 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": "@babel/template@^7.18.10", "@babel/template@^7.3.3":
version "7.18.10" version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@ -2840,6 +2847,11 @@
pluralize "^8.0.0" pluralize "^8.0.0"
yaml-ast-parser "0.0.43" 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": "@sinclair/typebox@^0.24.1":
version "0.24.51" version "0.24.51"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f"
@ -3243,11 +3255,6 @@
dependencies: dependencies:
"@types/node" "*" "@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": "@types/hoist-non-react-statics@^3.3.0":
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@ -3484,23 +3491,6 @@
dependencies: dependencies:
"@types/react" "*" "@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": "@types/react-test-renderer@^18.0.5":
version "18.0.5" version "18.0.5"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.5.tgz#b67a6ff37acd93d1b971ec4c838f69d52e772db0" 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" tiny-warning "^1.0.0"
value-equal "^1.0.1" 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: 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" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" 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" prop-types "^15.6.1"
shallow-equal "^1.1.0" 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: react-router-dom@5.3.3:
version "5.3.3" version "5.3.3"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.3.tgz#8779fc28e6691d07afcaf98406d3812fe6f11199" 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-invariant "^1.0.2"
tiny-warning "^1.0.0" 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: react-select-event@^5.1.0:
version "5.5.1" version "5.5.1"
resolved "https://registry.yarnpkg.com/react-select-event/-/react-select-event-5.5.1.tgz#d67e04a6a51428b1534b15ecb1b82afbe5edddcb" 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" resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== 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" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -13618,7 +13640,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" 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" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -13639,6 +13661,13 @@ strip-ansi@^5.2.0:
dependencies: dependencies:
ansi-regex "^4.1.0" 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: strip-ansi@^7.0.1:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" 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" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
name wrap-ansi-cjs
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -15003,6 +15031,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.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: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"