Grouping templating polishing 1st part (#1907)
# What this PR does - Design polishing of Integration Table, IntegrationForm - Pagination for Integrations2 page - Edit regexp route template modal - Bug fixes
This commit is contained in:
parent
f18858882e
commit
43b6e34c9e
25 changed files with 519 additions and 151 deletions
|
|
@ -20,7 +20,6 @@ test.skip('we can verify our phone number + receive an SMS alert', async ({ page
|
|||
// wait for the SMS alert notification to arrive
|
||||
const smsAlertNotification = await waitForSms();
|
||||
|
||||
console.log('SMS Alert Notification: ', smsAlertNotification);
|
||||
expect(smsAlertNotification).toContain('OnCall');
|
||||
expect(smsAlertNotification).toContain('alert');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -127,8 +127,8 @@ export const templateForEdit: { [id: string]: TemplateForEdit } = {
|
|||
displayName: 'Source link',
|
||||
description: '',
|
||||
},
|
||||
routing: {
|
||||
name: 'routing',
|
||||
route_template: {
|
||||
name: 'route_template',
|
||||
displayName: 'Routing',
|
||||
description:
|
||||
'Routes direct alerts to different escalation chains based on the content, such as severity or region.',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
width: 40%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
border: var(--border-weak);
|
||||
}
|
||||
|
||||
.cheatsheet-item {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const CheatSheet = (props: CheatSheetProps) => {
|
|||
<div className={cx('cheatsheet-container')}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>{cheatSheetData.name}</Text.Title>
|
||||
<Text strong>{cheatSheetData.name}</Text>
|
||||
<IconButton name="times" onClick={onClose} />
|
||||
</HorizontalGroup>
|
||||
<Text type="secondary">{cheatSheetData.description}</Text>
|
||||
|
|
@ -50,7 +50,7 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => {
|
|||
const { field } = props;
|
||||
return (
|
||||
<>
|
||||
<Text.Title level={4}>{field.name}</Text.Title>
|
||||
<Text>{field.name}</Text>
|
||||
{field.listItems?.map((item, key) => {
|
||||
return (
|
||||
<div key={key}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
.regexp-template-code {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.regexp-template-editor-modal {
|
||||
width: 700px;
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
import { HorizontalGroup, VerticalGroup, Modal, Tooltip, Icon, Button } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { TemplateForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
|
||||
import Block from 'components/GBlock/Block';
|
||||
import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor';
|
||||
import Text from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './EditRegexpRouteTemplateModal.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface EditRegexpRouteTemplateModalProps {
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
template?: TemplateForEdit;
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'];
|
||||
onHide: () => void;
|
||||
onUpdateRoute: (values: any, channelFilterId: ChannelFilter['id'], type: number) => void;
|
||||
onOpenEditIntegrationTemplate?: (templateName: string, channelFilterId: ChannelFilter['id']) => void;
|
||||
}
|
||||
|
||||
const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemplateModalProps) => {
|
||||
const { onHide, onUpdateRoute, channelFilterId, onOpenEditIntegrationTemplate, alertReceiveChannelId } = props;
|
||||
const store = useStore();
|
||||
const regexpBody = store.alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term;
|
||||
const [regexpTemplateBody, setRegexpTemplateBody] = useState<string>(regexpBody);
|
||||
|
||||
const templateJinja2Body = store.alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term_as_jinja2;
|
||||
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
const handleRegexpBodyChange = () => {
|
||||
return debounce((value: string) => {
|
||||
setRegexpTemplateBody(value);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
onUpdateRoute({ ['route_template']: regexpTemplateBody }, channelFilterId, 0);
|
||||
|
||||
onHide();
|
||||
}, [regexpTemplateBody]);
|
||||
|
||||
const handleConvertToJinja2 = useCallback(() => {
|
||||
alertReceiveChannelStore.convertRegexpTemplateToJinja2Template(channelFilterId).then((response) => {
|
||||
alertReceiveChannelStore
|
||||
.saveChannelFilter(channelFilterId, {
|
||||
filtering_term: response?.filtering_term_as_jinja2,
|
||||
filtering_term_type: 1,
|
||||
})
|
||||
.then(() => {
|
||||
alertReceiveChannelStore.updateChannelFilters(alertReceiveChannelId, true).then(() => {
|
||||
onOpenEditIntegrationTemplate('route_template', channelFilterId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onHide();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closeOnEscape
|
||||
isOpen
|
||||
onDismiss={onHide}
|
||||
title="Edit regular expression template"
|
||||
className={cx('regexp-template-editor-modal')}
|
||||
>
|
||||
<VerticalGroup spacing="lg">
|
||||
<VerticalGroup spacing="xs">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Text type={'secondary'}>Regular expression</Text>
|
||||
<Tooltip
|
||||
content={'Use python style regex to filter incidents based on a expression'}
|
||||
placement={'top-start'}
|
||||
>
|
||||
<Icon name={'info-circle'} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
|
||||
<div className={cx('regexp-template-code')}>
|
||||
<MonacoJinja2Editor
|
||||
value={regexpTemplateBody}
|
||||
height={'200px'}
|
||||
data={undefined}
|
||||
showLineNumbers={true}
|
||||
onChange={handleRegexpBodyChange()}
|
||||
/>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
<VerticalGroup>
|
||||
<Text>Click "Convert to Jinja2" for a rich editor with debugger and additional functionality</Text>
|
||||
<Text type={'secondary'}>Your template will be saved as the jinja2 template below</Text>
|
||||
</VerticalGroup>
|
||||
<Block bordered fullWidth withBackground>
|
||||
<Text type="link">{templateJinja2Body}</Text>
|
||||
</Block>
|
||||
|
||||
<HorizontalGroup justify={'flex-end'}>
|
||||
<Button variant={'secondary'} onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant={'secondary'} onClick={() => handleConvertToJinja2()}>
|
||||
Convert to Jinja2 template
|
||||
</Button>
|
||||
<Button variant={'primary'} onClick={() => handleSave()}>
|
||||
Save
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default EditRegexpRouteTemplateModal;
|
||||
|
|
@ -3,7 +3,7 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
|||
export function prepareForEdit(item: AlertReceiveChannel) {
|
||||
return {
|
||||
verbal_name: item.verbal_name,
|
||||
// description: item.description,
|
||||
description_short: item.description_short,
|
||||
team: item.team,
|
||||
};
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
gap: 24px;
|
||||
overflow: auto;
|
||||
scroll-snap-type: y mandatory;
|
||||
padding: 0 10px 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -33,12 +32,7 @@
|
|||
|
||||
.card_featured {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tag {
|
||||
top: 28px;
|
||||
right: 28px;
|
||||
position: absolute;
|
||||
height: 106px;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
@ -52,7 +46,7 @@
|
|||
}
|
||||
|
||||
.search-integration {
|
||||
width: 400px;
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +55,10 @@
|
|||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.collapse svg {
|
||||
color: var(--primary-text-link) !important;
|
||||
}
|
||||
|
||||
.collapsable-content {
|
||||
width: 100%;
|
||||
background-color: var(--background-secondary);
|
||||
|
|
@ -15,10 +15,10 @@ import { AlertReceiveChannelOption } from 'models/alert_receive_channel/alert_re
|
|||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import { form } from './IntegrationForm.config';
|
||||
import { prepareForEdit } from './IntegrationForm.helpers';
|
||||
import { form } from './IntegrationForm2.config';
|
||||
import { prepareForEdit } from './IntegrationForm2.helpers';
|
||||
|
||||
import styles from './IntegrationForm.module.css';
|
||||
import styles from './IntegrationForm2.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ interface IntegrationFormProps {
|
|||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const IntegrationForm = observer((props: IntegrationFormProps) => {
|
||||
const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
||||
const { id, onHide, onUpdate } = props;
|
||||
|
||||
const store = useStore();
|
||||
|
|
@ -39,7 +39,6 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [showNewIntegrationForm, setShowNewIntegrationForm] = useState(false);
|
||||
// const [showIntegrationListForm, setShowIntegrationListForm] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<AlertReceiveChannelOption>(undefined);
|
||||
|
||||
const data =
|
||||
|
|
@ -47,8 +46,6 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
? { integration: selectedOption?.value, team: user.current_team }
|
||||
: prepareForEdit(alertReceiveChannelStore.items[id]);
|
||||
|
||||
const integration = alertReceiveChannelStore.getIntegration(data);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(data: Partial<AlertReceiveChannel>) => {
|
||||
(id === 'new' ? alertReceiveChannelStore.create(data) : alertReceiveChannelStore.update(id, data)).then(() => {
|
||||
|
|
@ -81,7 +78,7 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
return (
|
||||
<>
|
||||
{id === 'new' && (
|
||||
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false}>
|
||||
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
<div className={cx('search-integration')}>
|
||||
|
|
@ -98,7 +95,8 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
return (
|
||||
<Block
|
||||
bordered
|
||||
shadowed
|
||||
hover
|
||||
withBackground
|
||||
onClick={handleNewIntegrationOptionSelectCallback(alertReceiveChannelChoice)}
|
||||
key={alertReceiveChannelChoice.value}
|
||||
className={cx('card', { card_featured: alertReceiveChannelChoice.featured })}
|
||||
|
|
@ -107,18 +105,18 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
<IntegrationLogo integration={alertReceiveChannelChoice} scale={0.2} />
|
||||
</div>
|
||||
<div className={cx('title')}>
|
||||
<VerticalGroup spacing="none">
|
||||
<Text strong data-testid="integration-display-name">
|
||||
{alertReceiveChannelChoice.display_name}
|
||||
</Text>
|
||||
<VerticalGroup spacing={alertReceiveChannelChoice.featured ? 'xs' : 'none'}>
|
||||
<HorizontalGroup>
|
||||
<Text strong data-testid="integration-display-name">
|
||||
{alertReceiveChannelChoice.display_name}
|
||||
</Text>
|
||||
{alertReceiveChannelChoice.featured && <Tag name="Quick connect" colorIndex={5} />}
|
||||
</HorizontalGroup>
|
||||
<Text type="secondary" size="small">
|
||||
{alertReceiveChannelChoice.short_description}
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
{alertReceiveChannelChoice.featured && (
|
||||
<Tag name="Quick connect" className={cx('tag')} colorIndex={7} />
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
})
|
||||
|
|
@ -133,13 +131,10 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
{(showNewIntegrationForm || id !== 'new') && (
|
||||
<Drawer
|
||||
scrollableContent
|
||||
title={
|
||||
id === 'new'
|
||||
? `New ${selectedOption?.display_name} integration`
|
||||
: `Edit ${integration?.display_name} integration`
|
||||
}
|
||||
title={id === 'new' ? `New ${selectedOption?.display_name} integration` : `Edit integration`}
|
||||
onClose={onHide}
|
||||
closeOnMaskClick={false}
|
||||
width="640px"
|
||||
>
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
|
|
@ -201,4 +196,4 @@ const IntegrationForm = observer((props: IntegrationFormProps) => {
|
|||
);
|
||||
});
|
||||
|
||||
export default IntegrationForm;
|
||||
export default IntegrationForm2;
|
||||
|
|
@ -1,24 +1,30 @@
|
|||
.title-container {
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
padding: 24px 24px 0;
|
||||
}
|
||||
|
||||
.container-wrapper {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border: var(--border-weak);
|
||||
border: var(--border-strong);
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.template-block-title {
|
||||
padding: 16px;
|
||||
align-items: baseline;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.template-editor-block-title {
|
||||
padding: 16px;
|
||||
padding: 8px 16px;
|
||||
align-items: baseline;
|
||||
border: var(--border-weak);
|
||||
background-color: var(--background-secondary);
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.template-block-list {
|
||||
|
|
@ -37,10 +43,9 @@
|
|||
}
|
||||
|
||||
.result {
|
||||
padding: 16px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.block-style {
|
||||
border: var(--border-weak);
|
||||
background-color: var(--background-secondary);
|
||||
.template-block-codeeditor div[aria-label='Code editor container'] {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Drawer, VerticalGroup } from '@grafana/ui';
|
||||
import { Button, HorizontalGroup, VerticalGroup, Icon, Drawer } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -19,6 +19,8 @@ import TemplatePreview from 'containers/TemplatePreview/TemplatePreview';
|
|||
import TemplatesAlertGroupsList from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import styles from './IntegrationTemplate.module.css';
|
||||
|
||||
|
|
@ -26,15 +28,16 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface IntegrationTemplateProps {
|
||||
id: AlertReceiveChannel['id'];
|
||||
channelFilterId?: ChannelFilter['id'];
|
||||
template: TemplateForEdit;
|
||||
templateBody: string;
|
||||
onHide: () => void;
|
||||
onUpdateTemplates: (values: any) => void;
|
||||
onUpdateRoute: (values: any) => void;
|
||||
onUpdateRoute: (values: any, channelFilterId?: ChannelFilter['id']) => void;
|
||||
}
|
||||
|
||||
const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
||||
const { id, onHide, template, onUpdateTemplates, onUpdateRoute, templateBody } = props;
|
||||
const { id, onHide, template, onUpdateTemplates, onUpdateRoute, templateBody, channelFilterId } = props;
|
||||
|
||||
const [isCheatSheetVisible, setIsCheatSheetVisible] = useState<boolean>(false);
|
||||
const [chatOps, setChatOps] = useState(undefined);
|
||||
|
|
@ -42,6 +45,17 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
const [changedTemplateBody, setChangedTemplateBody] = useState<string>(templateBody);
|
||||
const [resultError, setResultError] = useState<string>(undefined);
|
||||
|
||||
const locationParams: any = { template: template.name };
|
||||
if (template.isRoute) {
|
||||
locationParams.routeId = channelFilterId;
|
||||
}
|
||||
|
||||
LocationHelper.update(locationParams, 'partial');
|
||||
|
||||
useEffect(() => {
|
||||
LocationHelper.update(locationParams, 'partial');
|
||||
}, []);
|
||||
|
||||
const onShowCheatSheet = useCallback(() => {
|
||||
setIsCheatSheetVisible(true);
|
||||
}, []);
|
||||
|
|
@ -95,7 +109,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
|
||||
const handleSubmit = useCallback(() => {
|
||||
template.isRoute
|
||||
? onUpdateRoute({ [template.name]: changedTemplateBody })
|
||||
? onUpdateRoute({ [template.name]: changedTemplateBody }, channelFilterId)
|
||||
: onUpdateTemplates({ [template.name]: changedTemplateBody });
|
||||
|
||||
onHide();
|
||||
|
|
@ -128,31 +142,31 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title={
|
||||
<div className={cx('title-container')}>
|
||||
<HorizontalGroup justify="space-between" align="flex-start">
|
||||
<VerticalGroup>
|
||||
<Text.Title level={3}>Edit {template.displayName} template</Text.Title>
|
||||
{template.description && <Text type="secondary">{template.description}</Text>}
|
||||
</VerticalGroup>
|
||||
<Drawer
|
||||
title={
|
||||
<div className={cx('title-container')}>
|
||||
<HorizontalGroup justify="space-between" align="flex-start">
|
||||
<VerticalGroup>
|
||||
<Text.Title level={3}>Edit {template.displayName} template</Text.Title>
|
||||
{template.description && <Text type="secondary">{template.description}</Text>}
|
||||
</VerticalGroup>
|
||||
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
}
|
||||
onClose={onHide}
|
||||
closeOnMaskClick={false}
|
||||
width={'95%'}
|
||||
>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
}
|
||||
onClose={onHide}
|
||||
closeOnMaskClick={false}
|
||||
width={'95%'}
|
||||
>
|
||||
<div className={cx('container-wrapper')}>
|
||||
<div className={cx('container')}>
|
||||
<TemplatesAlertGroupsList
|
||||
alertReceiveChannelId={id}
|
||||
|
|
@ -178,7 +192,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
value={templateBody}
|
||||
data={undefined}
|
||||
showLineNumbers={true}
|
||||
height={'100vh'}
|
||||
height={'85vh'}
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -203,8 +217,8 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -222,6 +236,9 @@ interface ResultProps {
|
|||
const Result = (props: ResultProps) => {
|
||||
const { alertReceiveChannelId, templateName, chatOps, payload, templateBody, error, onSaveAndFollowLink } = props;
|
||||
|
||||
const getCapitalizedChatopsName = (name: string) => {
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
};
|
||||
return (
|
||||
<div className={cx('template-block-result')}>
|
||||
<div className={cx('template-block-title')}>
|
||||
|
|
@ -237,7 +254,7 @@ const Result = (props: ResultProps) => {
|
|||
<Text>{error}</Text>
|
||||
</Block>
|
||||
) : (
|
||||
<Block bordered fullWidth className={cx('block-style')}>
|
||||
<Block bordered fullWidth withBackground>
|
||||
<TemplatePreview
|
||||
key={templateName}
|
||||
templateName={templateName}
|
||||
|
|
@ -251,7 +268,10 @@ const Result = (props: ResultProps) => {
|
|||
{chatOps && (
|
||||
<VerticalGroup>
|
||||
<Button onClick={() => onSaveAndFollowLink(chatOps.permalink)}>
|
||||
Save and open Alert Group in {chatOps.name}
|
||||
<HorizontalGroup spacing="xs" align="center">
|
||||
Save and open Alert Group in {getCapitalizedChatopsName(chatOps.name)}{' '}
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Button>
|
||||
|
||||
{chatOps.comment && (
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder, Alert as AlertComponent } from '@grafana/ui';
|
||||
import { HorizontalGroup, Icon, LoadingPlaceholder } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -64,18 +65,23 @@ const TemplatePreview = observer((props: TemplatePreviewProps) => {
|
|||
return result ? (
|
||||
<>
|
||||
{templateName.includes('condition_template') ? (
|
||||
<AlertComponent severity={isCondition ? 'success' : 'error'} title="">
|
||||
<Text type={isCondition ? 'success' : 'danger'}>
|
||||
{isCondition ? (
|
||||
'True'
|
||||
<>
|
||||
<Icon name="check" size="lg" /> True
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={cx('message')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(result.preview || ''),
|
||||
}}
|
||||
/>
|
||||
<HorizontalGroup>
|
||||
<Icon name="exclamation-triangle" size="lg" />
|
||||
<div
|
||||
className={cx('message')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(result.preview || ''),
|
||||
}}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
</AlertComponent>
|
||||
</Text>
|
||||
) : (
|
||||
<div
|
||||
className={cx('message')}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
.template-block-title {
|
||||
padding: 16px;
|
||||
padding: 16px 16px 16px 0;
|
||||
align-items: baseline;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.template-block-list {
|
||||
|
|
@ -11,4 +12,13 @@
|
|||
.alert-group-payload-view {
|
||||
background-color: var(--primary-background);
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.alert-groups-list > div {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.alert-groups-list button {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
<MonacoJinja2Editor
|
||||
value={JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
data={undefined}
|
||||
height={'100vh'}
|
||||
height={'85vh'}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
/>
|
||||
|
|
@ -101,8 +101,8 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
<VerticalGroup>
|
||||
<Badge style={{ margin: '16px' }} color="blue" text="Last alert payload" />
|
||||
<SourceCode className={cx('alert-group-payload-view')} noMaxHeight>
|
||||
<Badge color="blue" text="Last alert payload" />
|
||||
<SourceCode className={cx('alert-group-payload-view')} noMaxHeight showClipboardIconOnly>
|
||||
{JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
</SourceCode>
|
||||
</VerticalGroup>
|
||||
|
|
@ -127,7 +127,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
<MonacoJinja2Editor
|
||||
value={null}
|
||||
data={undefined}
|
||||
height={'100vh'}
|
||||
height={'85vh'}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
/>
|
||||
|
|
@ -136,7 +136,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup justify="space-between" wrap>
|
||||
<HorizontalGroup>
|
||||
<Text>Recent Alert groups</Text>
|
||||
<Tooltip content="Here will be information about alert groups">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export interface AlertReceiveChannel {
|
|||
integration: string;
|
||||
smile_code: string;
|
||||
verbal_name: string;
|
||||
description: string;
|
||||
description_short: string;
|
||||
author: User['pk'];
|
||||
team: GrafanaTeam['id'];
|
||||
created_at: string;
|
||||
|
|
@ -26,6 +28,7 @@ export interface AlertReceiveChannel {
|
|||
maintenance_till?: number;
|
||||
heartbeat: Heartbeat | null;
|
||||
is_available_for_integration_heartbeat: boolean;
|
||||
routes_count: number;
|
||||
}
|
||||
|
||||
export interface AlertReceiveChannelChoice {
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ import {
|
|||
|
||||
export class AlertReceiveChannelStore extends BaseStore {
|
||||
@observable.shallow
|
||||
// searchResult: { count?: number; results?: Array<AlertReceiveChannel['id']> } = {};
|
||||
searchResult: Array<AlertReceiveChannel['id']>;
|
||||
|
||||
@observable.shallow
|
||||
paginatedSearchResult: { count?: number; results?: Array<AlertReceiveChannel['id']> } = {};
|
||||
|
||||
@observable.shallow
|
||||
items: { [id: string]: AlertReceiveChannel } = {};
|
||||
|
||||
|
|
@ -78,6 +80,21 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
// };
|
||||
}
|
||||
|
||||
getPaginatedSearchResult(_query = '') {
|
||||
if (!this.paginatedSearchResult) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
count: this.paginatedSearchResult.count,
|
||||
results:
|
||||
this.paginatedSearchResult.results &&
|
||||
this.paginatedSearchResult.results.map(
|
||||
(alertReceiveChannelId: AlertReceiveChannel['id']) => this.items?.[alertReceiveChannelId]
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
async loadItem(id: AlertReceiveChannel['id'], skipErrorHandling = false): Promise<AlertReceiveChannel> {
|
||||
const alertReceiveChannel = await this.getById(id, skipErrorHandling);
|
||||
|
|
@ -161,6 +178,59 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return result;
|
||||
}
|
||||
|
||||
async updatePaginatedItems(query: any = '', page = 1) {
|
||||
const filters = typeof query === 'string' ? { search: query } : query;
|
||||
const { search } = filters;
|
||||
const { count, results } = await makeRequest(this.path, { params: { search, page } });
|
||||
|
||||
this.items = {
|
||||
...this.items,
|
||||
...results.reduce(
|
||||
(acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
|
||||
...acc,
|
||||
[item.id]: omit(item, 'heartbeat'),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
};
|
||||
|
||||
this.paginatedSearchResult = results.map((item: AlertReceiveChannel) => item.id);
|
||||
this.paginatedSearchResult = {
|
||||
count,
|
||||
results: results.map((item: AlertReceiveChannel) => item.id),
|
||||
};
|
||||
|
||||
const heartbeats = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.heartbeat.id] = alertReceiveChannel.heartbeat;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.rootStore.heartbeatStore.items = {
|
||||
...this.rootStore.heartbeatStore.items,
|
||||
...heartbeats,
|
||||
};
|
||||
|
||||
const alertReceiveChannelToHeartbeat = results.reduce((acc: any, alertReceiveChannel: AlertReceiveChannel) => {
|
||||
if (alertReceiveChannel.heartbeat) {
|
||||
acc[alertReceiveChannel.id] = alertReceiveChannel.heartbeat.id;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.alertReceiveChannelToHeartbeat = {
|
||||
...this.alertReceiveChannelToHeartbeat,
|
||||
...alertReceiveChannelToHeartbeat,
|
||||
};
|
||||
|
||||
this.updateCounters();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@action
|
||||
async updateChannelFilters(alertReceiveChannelId: AlertReceiveChannel['id'], isOverwrite = false) {
|
||||
const response = await makeRequest(`/channel_filters/`, {
|
||||
|
|
@ -379,6 +449,13 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
await makeRequest(`/channel_filters/${id}/send_demo_alert/`, { method: 'POST' }).catch(showApiError);
|
||||
}
|
||||
|
||||
async convertRegexpTemplateToJinja2Template(id: ChannelFilter['id']) {
|
||||
const result = await makeRequest(`/channel_filters/${id}/convert_from_regex_to_jinja2/`, { method: 'POST' }).catch(
|
||||
showApiError
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async renderPreview(id: AlertReceiveChannel['id'], template_name: string, template_body: string, payload: JSON) {
|
||||
return await makeRequest(`${this.path}${id}/preview_template/`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface AlertReceiveChannel {
|
|||
is_available_for_integration_heartbeat: boolean;
|
||||
allow_delete: boolean;
|
||||
deleted?: boolean;
|
||||
routes_count: number;
|
||||
}
|
||||
|
||||
export interface AlertReceiveChannelOption {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface ChannelFilter {
|
|||
telegram_channel?: TelegramChannel['id'];
|
||||
created_at: string;
|
||||
filtering_term: string;
|
||||
filtering_term_as_jinja2: string;
|
||||
filtering_term_type: FilteringTermType;
|
||||
is_default: boolean;
|
||||
notify_in_slack: boolean;
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export const pages: { [id: string]: PageDefinition } = [
|
|||
id: 'integrations_2',
|
||||
text: 'Integrations 2',
|
||||
path: getPath('integrations_2'),
|
||||
hideTitle: true,
|
||||
hideFromBreadcrumbs: true,
|
||||
hideFromTabs: true,
|
||||
action: UserActions.IntegrationsRead,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,12 @@ interface ExpandedIntegrationRouteDisplayProps {
|
|||
channelFilterId: ChannelFilter['id'];
|
||||
routeIndex: number;
|
||||
templates: AlertTemplatesDTO[];
|
||||
openEditTemplateModal: (templateName: string | string[]) => void;
|
||||
openEditTemplateModal: (templateName: string | string[], channelFilterId?: ChannelFilter['id']) => void;
|
||||
onEditRegexpTemplate: (
|
||||
templateRegexpBody: string,
|
||||
templateJijja2Body: string,
|
||||
channelFilterId: ChannelFilter['id']
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface ExpandedIntegrationRouteDisplayState {
|
||||
|
|
@ -44,7 +49,7 @@ interface ExpandedIntegrationRouteDisplayState {
|
|||
}
|
||||
|
||||
const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayProps> = observer(
|
||||
({ alertReceiveChannelId, channelFilterId, templates, routeIndex, openEditTemplateModal }) => {
|
||||
({ alertReceiveChannelId, channelFilterId, templates, routeIndex, openEditTemplateModal, onEditRegexpTemplate }) => {
|
||||
const { escalationPolicyStore, escalationChainStore, alertReceiveChannelStore, grafanaTeamStore } = useStore();
|
||||
const hasChatOpsConnectors = false;
|
||||
|
||||
|
|
@ -106,8 +111,13 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
<Button variant={'secondary'} icon="edit" size={'md'} onClick={undefined} />
|
||||
<Button variant="secondary" size="md" onClick={() => openEditTemplateModal('routing')}>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
icon="edit"
|
||||
size={'md'}
|
||||
onClick={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
||||
/>
|
||||
<Button variant="secondary" size="md" onClick={undefined}>
|
||||
<Text type="link">Help</Text>
|
||||
<Icon name="angle-down" size="sm" />
|
||||
</Button>
|
||||
|
|
@ -229,6 +239,14 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
await escalationChainStore.updateItems();
|
||||
setState({ isRefreshingEscalationChains: false });
|
||||
}
|
||||
|
||||
function handleEditRoutingTemplate(channelFilter, channelFilterId) {
|
||||
if (channelFilter.filtering_term_type === 0) {
|
||||
onEditRegexpTemplate(channelFilter.filtering_term, channelFilter.filtering_term_as_jinja2, channelFilterId);
|
||||
} else {
|
||||
openEditTemplateModal('route_template', channelFilterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ const IntegrationHelper = {
|
|||
const totalMinDiff = minDiff - hourDiff * 60;
|
||||
const totalDiffString = `${hourDiff}h ${totalMinDiff}m left`;
|
||||
|
||||
if (mode) {
|
||||
if (mode !== undefined) {
|
||||
return `${mode === MaintenanceMode.Debug ? 'Debug Maintenance' : 'Maintenance'}: ${totalDiffString}`;
|
||||
}
|
||||
|
||||
return totalDiffString;
|
||||
return `${hourDiff}h left`;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import Text from 'components/Text/Text';
|
|||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
|
||||
import EditRegexpRouteTemplateModal from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal';
|
||||
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import UserDisplayWithAvatar from 'containers/UserDisplay/UserDisplayWithAvatar';
|
||||
|
|
@ -45,6 +46,7 @@ import { useStore } from 'state/useStore';
|
|||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { openNotification, openErrorNotification } from 'utils';
|
||||
import { getVar } from 'utils/DOM';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { DATASOURCE_ALERTING, PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
|
|
@ -64,6 +66,9 @@ interface Integration2State extends PageBaseState {
|
|||
isDemoModalOpen: boolean;
|
||||
isEditTemplateModalOpen: boolean;
|
||||
selectedTemplate: TemplateForEdit;
|
||||
isEditRegexpRouteTemplateModalOpen: boolean;
|
||||
channelFilterIdForEdit: ChannelFilter['id'];
|
||||
isNewRoute: boolean;
|
||||
}
|
||||
|
||||
// This can be further improved by using a ref instead
|
||||
|
|
@ -80,6 +85,9 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
isDemoModalOpen: false,
|
||||
isEditTemplateModalOpen: false,
|
||||
selectedTemplate: undefined,
|
||||
isEditRegexpRouteTemplateModalOpen: false,
|
||||
channelFilterIdForEdit: undefined,
|
||||
isNewRoute: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -88,16 +96,28 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
match: {
|
||||
params: { id },
|
||||
},
|
||||
query,
|
||||
} = this.props;
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
|
||||
if (query?.template) {
|
||||
this.openEditTemplateModal(query.template, query.routeId && query.routeId);
|
||||
}
|
||||
await Promise.all([this.loadIntegration(), alertReceiveChannelStore.updateTemplates(id)]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorData, isDemoModalOpen, isEditTemplateModalOpen, selectedTemplate } = this.state;
|
||||
const {
|
||||
errorData,
|
||||
isDemoModalOpen,
|
||||
isEditTemplateModalOpen,
|
||||
selectedTemplate,
|
||||
isEditRegexpRouteTemplateModalOpen,
|
||||
channelFilterIdForEdit,
|
||||
isNewRoute,
|
||||
} = this.state;
|
||||
const {
|
||||
store: { alertReceiveChannelStore, grafanaTeamStore },
|
||||
match: {
|
||||
|
|
@ -342,7 +362,7 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
<VerticalGroup spacing="md">
|
||||
<Text type={'primary'}>Routes</Text>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button variant={'primary'} onClick={() => this.openEditTemplateModal('routing')}>
|
||||
<Button variant={'primary'} onClick={this.handleAddNewRoute}>
|
||||
Add route
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
|
|
@ -365,12 +385,29 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
onHide={() => {
|
||||
this.setState({
|
||||
isEditTemplateModalOpen: undefined,
|
||||
isNewRoute: false,
|
||||
});
|
||||
LocationHelper.update({ template: undefined, routeId: undefined }, 'partial');
|
||||
}}
|
||||
channelFilterId={channelFilterIdForEdit}
|
||||
onUpdateTemplates={this.onUpdateTemplatesCallback}
|
||||
onUpdateRoute={this.onUpdateRoutesCallback}
|
||||
onUpdateRoute={isNewRoute ? this.onCreateRoutesCallback : this.onUpdateRoutesCallback}
|
||||
template={selectedTemplate}
|
||||
templateBody={templates[selectedTemplate?.name]}
|
||||
templateBody={
|
||||
selectedTemplate?.name === 'route_template'
|
||||
? this.getRoutingTemplate(isNewRoute, channelFilterIdForEdit)
|
||||
: templates[selectedTemplate?.name]
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isEditRegexpRouteTemplateModalOpen && (
|
||||
<EditRegexpRouteTemplateModal
|
||||
alertReceiveChannelId={id}
|
||||
channelFilterId={channelFilterIdForEdit}
|
||||
template={selectedTemplate}
|
||||
onHide={() => this.setState({ isEditRegexpRouteTemplateModalOpen: false })}
|
||||
onUpdateRoute={this.onUpdateRoutesCallback}
|
||||
onOpenEditIntegrationTemplate={this.openEditTemplateModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -379,6 +416,21 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
);
|
||||
}
|
||||
|
||||
getRoutingTemplate = (isRouteNew: boolean, channelFilterId: ChannelFilter['id']) => {
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
if (isRouteNew) {
|
||||
return '{{ (payload.severity == "foo" and "bar" in payload.region) or True }}';
|
||||
} else {
|
||||
return alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term;
|
||||
}
|
||||
};
|
||||
handleAddNewRoute = () => {
|
||||
this.setState({ isNewRoute: true });
|
||||
this.openEditTemplateModal('route_template');
|
||||
};
|
||||
|
||||
renderRoutesFn = (): IntegrationCollapsibleItem[] => {
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
|
|
@ -407,6 +459,7 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
routeIndex={routeIndex}
|
||||
templates={templates}
|
||||
openEditTemplateModal={this.openEditTemplateModal}
|
||||
onEditRegexpTemplate={this.handleEditRegexpRouteTemplate}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
|
@ -450,7 +503,11 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
|
||||
handleSlackChannelChange = () => {};
|
||||
|
||||
onUpdateRoutesCallback = ({ routing }: { routing: string }) => {
|
||||
handleEditRegexpRouteTemplate = (channelFilterId) => {
|
||||
this.setState({ isEditRegexpRouteTemplateModalOpen: true, channelFilterIdForEdit: channelFilterId });
|
||||
};
|
||||
|
||||
onCreateRoutesCallback = ({ route_template }: { route_template: string }) => {
|
||||
const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store;
|
||||
const {
|
||||
params: { id },
|
||||
|
|
@ -460,7 +517,7 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
.createChannelFilter({
|
||||
order: 0,
|
||||
alert_receive_channel: id,
|
||||
filtering_term: routing,
|
||||
filtering_term: route_template,
|
||||
|
||||
// TODO: need to figure out this value
|
||||
filtering_term_type: 1,
|
||||
|
|
@ -479,6 +536,37 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
});
|
||||
};
|
||||
|
||||
onUpdateRoutesCallback = (
|
||||
{ route_template }: { route_template: string },
|
||||
channelFilterId,
|
||||
filteringTermType?: number
|
||||
) => {
|
||||
const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store;
|
||||
const {
|
||||
params: { id },
|
||||
} = this.props.match;
|
||||
|
||||
alertReceiveChannelStore
|
||||
.saveChannelFilter(channelFilterId, {
|
||||
filtering_term: route_template,
|
||||
|
||||
// TODO: need to figure out this value
|
||||
filtering_term_type: filteringTermType,
|
||||
})
|
||||
.then((channelFilter: ChannelFilter) => {
|
||||
alertReceiveChannelStore.updateChannelFilters(id, true).then(() => {
|
||||
// @ts-ignore
|
||||
escalationPolicyStore.updateEscalationPolicies(channelFilter.escalation_chain);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
const errors = get(err, 'response.data');
|
||||
if (errors?.non_field_errors) {
|
||||
openErrorNotification(errors.non_field_errors);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onUpdateTemplatesCallback = (data) => {
|
||||
const {
|
||||
store,
|
||||
|
|
@ -503,9 +591,13 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
|
||||
getTemplatesList = (): CascaderOption[] => INTEGRATION_TEMPLATES_LIST;
|
||||
|
||||
openEditTemplateModal = (templateName) => {
|
||||
this.setState({ isEditTemplateModalOpen: true });
|
||||
openEditTemplateModal = (templateName, channelFilterId?: ChannelFilter['id']) => {
|
||||
this.setState({ selectedTemplate: templateForEdit[templateName] });
|
||||
this.setState({ isEditTemplateModalOpen: true });
|
||||
|
||||
if (channelFilterId) {
|
||||
this.setState({ channelFilterIdForEdit: channelFilterId });
|
||||
}
|
||||
};
|
||||
|
||||
onRemovalFn = (id: AlertReceiveChannel['id']) => {
|
||||
|
|
|
|||
|
|
@ -6,3 +6,16 @@
|
|||
margin-bottom: 24px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.integrations-table-row {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.integrations-table {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.heartbeat-badge {
|
||||
padding: 4px 10px;
|
||||
width: 40px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HorizontalGroup, Badge, Tooltip, Button, IconButton } from '@grafana/ui';
|
||||
import { HorizontalGroup, Button, IconButton } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -19,13 +19,12 @@ import PluginLink from 'components/PluginLink/PluginLink';
|
|||
import Text from 'components/Text/Text';
|
||||
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import IntegrationForm from 'containers/IntegrationForm/IntegrationForm';
|
||||
import IntegrationForm2 from 'containers/IntegrationForm/IntegrationForm2';
|
||||
import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { HeartGreenIcon, HeartRedIcon } from 'icons';
|
||||
import { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel';
|
||||
import { MaintenanceType } from 'models/maintenance/maintenance.types';
|
||||
import IntegrationHelper from 'pages/integration_2/Integration2.helper';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
|
@ -36,7 +35,7 @@ import styles from './Integrations2.module.scss';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
const FILTERS_DEBOUNCE_MS = 500;
|
||||
// const ITEMS_PER_PAGE = 25;
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
|
||||
interface IntegrationsState extends PageBaseState {
|
||||
integrationsFilters: Filters;
|
||||
|
|
@ -105,15 +104,15 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
const { page, integrationsFilters } = this.state;
|
||||
LocationHelper.update({ p: page }, 'partial');
|
||||
|
||||
return store.alertReceiveChannelStore.updateItems(integrationsFilters);
|
||||
return store.alertReceiveChannelStore.updatePaginatedItems(integrationsFilters, page);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { store, query } = this.props;
|
||||
const { alertReceiveChannelId } = this.state;
|
||||
const { alertReceiveChannelId, page } = this.state;
|
||||
const { grafanaTeamStore, alertReceiveChannelStore, heartbeatStore } = store;
|
||||
|
||||
const results = alertReceiveChannelStore.getSearchResult();
|
||||
const { count, results } = alertReceiveChannelStore.getPaginatedSearchResult();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
|
@ -164,7 +163,8 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('title')}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>Integrations 2</Text.Title>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
@ -190,16 +190,18 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
rowKey="id"
|
||||
data={results}
|
||||
columns={columns}
|
||||
// pagination={{
|
||||
// page,
|
||||
// total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
// onChange: this.handleChangePage,
|
||||
// }}
|
||||
className={cx('integrations-table')}
|
||||
rowClassName={cx('integrations-table-row')}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{alertReceiveChannelId && (
|
||||
<IntegrationForm
|
||||
<IntegrationForm2
|
||||
onHide={() => {
|
||||
this.setState({ alertReceiveChannelId: undefined });
|
||||
}}
|
||||
|
|
@ -239,25 +241,24 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
return (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<IntegrationLogo scale={0.08} integration={integration} />
|
||||
<Text type="secondary" size="small">
|
||||
{integration?.display_name}
|
||||
</Text>
|
||||
<Text type="secondary">{integration?.display_name}</Text>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderIntegrationStatus(item: AlertReceiveChannel, alertReceiveChannelStore) {
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[item.id];
|
||||
let routesCounter = undefined;
|
||||
let routesCounter = item.routes_count;
|
||||
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup spacing="xs">
|
||||
{alertReceiveChannelCounter && (
|
||||
<PluginLink query={{ page: 'incidents', integration: item.id }} className={cx('alertsInfoText')}>
|
||||
<Badge
|
||||
<TooltipBadge
|
||||
borderType="primary"
|
||||
text={alertReceiveChannelCounter?.alerts_count + '/' + alertReceiveChannelCounter?.alert_groups_count}
|
||||
color={'blue'}
|
||||
tooltip={
|
||||
tooltipTitle=""
|
||||
tooltipContent={
|
||||
alertReceiveChannelCounter?.alerts_count +
|
||||
' alert' +
|
||||
(alertReceiveChannelCounter?.alerts_count === 1 ? '' : 's') +
|
||||
|
|
@ -269,7 +270,15 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
/>
|
||||
</PluginLink>
|
||||
)}
|
||||
{routesCounter && <Badge text={routesCounter} color={'green'} tooltip={`${routesCounter} routes`} />}
|
||||
{routesCounter && (
|
||||
<TooltipBadge
|
||||
borderType="success"
|
||||
icon="link"
|
||||
text={routesCounter}
|
||||
tooltipTitle=""
|
||||
tooltipContent={`${routesCounter} routes`}
|
||||
/>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
|
@ -282,20 +291,16 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
return (
|
||||
<div className={cx('heartbeat')}>
|
||||
<div>
|
||||
{alertReceiveChannel.is_available_for_integration_heartbeat && (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
content={
|
||||
heartbeat
|
||||
? `Last heartbeat: ${heartbeat.last_heartbeat_time_verbal || 'never'}`
|
||||
: 'Click to setup heartbeat'
|
||||
}
|
||||
>
|
||||
<div className={cx('heartbeat-icon')} onClick={() => {}}>
|
||||
{heartbeatStatus ? <HeartGreenIcon /> : <HeartRedIcon />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<TooltipBadge
|
||||
text={undefined}
|
||||
className={cx('heartbeat-badge')}
|
||||
borderType={heartbeat?.last_heartbeat_time_verbal ? 'success' : 'danger'}
|
||||
customIcon={heartbeatStatus ? <HeartGreenIcon /> : <HeartRedIcon />}
|
||||
tooltipTitle={`Last heartbeat: ${heartbeat?.last_heartbeat_time_verbal || 'never'}`}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -311,7 +316,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
borderType="primary"
|
||||
icon="pause"
|
||||
text={IntegrationHelper.getMaintenanceText(item.maintenance_till)}
|
||||
tooltipTitle={IntegrationHelper.getMaintenanceText(item.maintenance_till, item.maintenance_mode)}
|
||||
tooltipTitle={IntegrationHelper.getMaintenanceText(item.maintenance_till, maintenanceMode)}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -321,12 +326,6 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
return null;
|
||||
}
|
||||
|
||||
handleStopMaintenance = (item: AlertReceiveChannel, maintenanceStore, alertReceiveChannelStore) => {
|
||||
maintenanceStore.stopMaintenanceMode(MaintenanceType.alert_receive_channel, item.id).then(() => {
|
||||
alertReceiveChannelStore.updateItem(item.id);
|
||||
});
|
||||
};
|
||||
|
||||
renderTeam(item: AlertReceiveChannel, teams: any) {
|
||||
return <TeamName team={teams[item.team]} />;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue