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:
Yulia Shanyrova 2023-05-15 10:07:04 +02:00 committed by GitHub
parent f18858882e
commit 43b6e34c9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 519 additions and 151 deletions

View file

@ -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');
});

View file

@ -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.',

View file

@ -2,6 +2,7 @@
width: 40%;
height: 100%;
padding: 16px;
border: var(--border-weak);
}
.cheatsheet-item {

View file

@ -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}>

View file

@ -0,0 +1,7 @@
.regexp-template-code {
width: 100%;
}
.regexp-template-editor-modal {
width: 700px;
}

View file

@ -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;

View file

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

View file

@ -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);

View file

@ -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;

View file

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

View file

@ -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 && (

View file

@ -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')}

View file

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

View file

@ -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">

View file

@ -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 {

View file

@ -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',

View file

@ -31,6 +31,7 @@ export interface AlertReceiveChannel {
is_available_for_integration_heartbeat: boolean;
allow_delete: boolean;
deleted?: boolean;
routes_count: number;
}
export interface AlertReceiveChannelOption {

View file

@ -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;

View file

@ -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,

View file

@ -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);
}
}
}
);

View file

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

View file

@ -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']) => {

View file

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

View file

@ -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]} />;
}