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:
parent
be1dd672df
commit
0aa3b1dc33
32 changed files with 1105 additions and 969 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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/,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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: '',
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
);
|
||||||
|
|
|
||||||
220
grafana-plugin/src/pages/incident/Incident.styles.ts
Normal file
220
grafana-plugin/src/pages/incident/Incident.styles.ts
Normal 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;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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)));
|
|
||||||
|
|
|
||||||
116
grafana-plugin/src/pages/incidents/Incidents.styles.ts
Normal file
116
grafana-plugin/src/pages/incidents/Incidents.styles.ts
Normal 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%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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)));
|
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
||||||
|
|
|
||||||
368
grafana-plugin/src/pages/integration/IntegrationActions.tsx
Normal file
368
grafana-plugin/src/pages/integration/IntegrationActions.tsx
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -86,4 +86,4 @@ const config = async (env): Promise<Configuration> => {
|
||||||
})(baseConfig, customConfig);
|
})(baseConfig, customConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue