From 43b6e34c9e8606b030cd3762c2f783e102b07dc5 Mon Sep 17 00:00:00 2001 From: Yulia Shanyrova Date: Mon, 15 May 2023 10:07:04 +0200 Subject: [PATCH] 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 --- .../integration-tests/alerts/sms.test.ts | 1 - .../AlertTemplatesForm.config.ts | 4 +- .../CheatSheet/CheatSheet.module.css | 1 + .../src/components/CheatSheet/CheatSheet.tsx | 4 +- .../EditRegexpRouteTemplateModal.module.css | 7 + .../EditRegexpRouteTemplateModal.tsx | 122 ++++++++++++++++++ ...m.config.ts => IntegrationForm2.config.ts} | 0 ...helpers.ts => IntegrationForm2.helpers.ts} | 2 +- ...module.css => IntegrationForm2.module.css} | 14 +- ...tegrationForm.tsx => IntegrationForm2.tsx} | 39 +++--- .../IntegrationTemplate.module.css | 21 +-- .../IntegrationTemplate.tsx | 86 +++++++----- .../TemplatePreview/TemplatePreview.tsx | 26 ++-- .../TemplatesAlertGroupsList.module.css | 12 +- .../TemplatesAlertGroupsList.tsx | 10 +- .../src/models/alert_receive_channel.ts | 3 + .../alert_receive_channel.ts | 79 +++++++++++- .../alert_receive_channel.types.ts | 1 + .../channel_filter/channel_filter.types.ts | 1 + grafana-plugin/src/pages/index.tsx | 1 + .../ExpandedIntegrationRouteDisplay.tsx | 26 +++- .../integration_2/Integration2.helper.ts | 4 +- .../src/pages/integration_2/Integration2.tsx | 108 ++++++++++++++-- .../integrations_2/Integrations2.module.scss | 13 ++ .../pages/integrations_2/Integrations2.tsx | 85 ++++++------ 25 files changed, 519 insertions(+), 151 deletions(-) create mode 100644 grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.module.css create mode 100644 grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx rename grafana-plugin/src/containers/IntegrationForm/{IntegrationForm.config.ts => IntegrationForm2.config.ts} (100%) rename grafana-plugin/src/containers/IntegrationForm/{IntegrationForm.helpers.ts => IntegrationForm2.helpers.ts} (81%) rename grafana-plugin/src/containers/IntegrationForm/{IntegrationForm.module.css => IntegrationForm2.module.css} (90%) rename grafana-plugin/src/containers/IntegrationForm/{IntegrationForm.tsx => IntegrationForm2.tsx} (86%) diff --git a/grafana-plugin/integration-tests/alerts/sms.test.ts b/grafana-plugin/integration-tests/alerts/sms.test.ts index f4a1d8f7..070eef89 100644 --- a/grafana-plugin/integration-tests/alerts/sms.test.ts +++ b/grafana-plugin/integration-tests/alerts/sms.test.ts @@ -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'); }); diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.config.ts b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.config.ts index 2a5c6f61..623eec63 100644 --- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.config.ts +++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.config.ts @@ -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.', diff --git a/grafana-plugin/src/components/CheatSheet/CheatSheet.module.css b/grafana-plugin/src/components/CheatSheet/CheatSheet.module.css index 07ab4254..3093ecb2 100644 --- a/grafana-plugin/src/components/CheatSheet/CheatSheet.module.css +++ b/grafana-plugin/src/components/CheatSheet/CheatSheet.module.css @@ -2,6 +2,7 @@ width: 40%; height: 100%; padding: 16px; + border: var(--border-weak); } .cheatsheet-item { diff --git a/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx b/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx index 0768e551..1649df3b 100644 --- a/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx +++ b/grafana-plugin/src/components/CheatSheet/CheatSheet.tsx @@ -25,7 +25,7 @@ const CheatSheet = (props: CheatSheetProps) => {
- {cheatSheetData.name} + {cheatSheetData.name} {cheatSheetData.description} @@ -50,7 +50,7 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => { const { field } = props; return ( <> - {field.name} + {field.name} {field.listItems?.map((item, key) => { return (
diff --git a/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.module.css b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.module.css new file mode 100644 index 00000000..988421d5 --- /dev/null +++ b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.module.css @@ -0,0 +1,7 @@ +.regexp-template-code { + width: 100%; +} + +.regexp-template-editor-modal { + width: 700px; +} diff --git a/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx new file mode 100644 index 00000000..0e1b1e33 --- /dev/null +++ b/grafana-plugin/src/containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal.tsx @@ -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(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 ( + + + + + Regular expression + + + + + +
+ +
+
+ + Click "Convert to Jinja2" for a rich editor with debugger and additional functionality + Your template will be saved as the jinja2 template below + + + {templateJinja2Body} + + + + + + + +
+
+ ); +}); + +export default EditRegexpRouteTemplateModal; diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.ts b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.config.ts similarity index 100% rename from grafana-plugin/src/containers/IntegrationForm/IntegrationForm.config.ts rename to grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.config.ts diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.helpers.ts b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.helpers.ts similarity index 81% rename from grafana-plugin/src/containers/IntegrationForm/IntegrationForm.helpers.ts rename to grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.helpers.ts index 13afcde8..d2bdfae2 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.helpers.ts +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.helpers.ts @@ -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, }; } diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.module.css b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.module.css similarity index 90% rename from grafana-plugin/src/containers/IntegrationForm/IntegrationForm.module.css rename to grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.module.css index 4d144905..4b56b1bf 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.module.css +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.module.css @@ -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); diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.tsx similarity index 86% rename from grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx rename to grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.tsx index 4092f2f6..f8a308b5 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.tsx @@ -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(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) => { (id === 'new' ? alertReceiveChannelStore.create(data) : alertReceiveChannelStore.update(id, data)).then(() => { @@ -81,7 +78,7 @@ const IntegrationForm = observer((props: IntegrationFormProps) => { return ( <> {id === 'new' && ( - +
@@ -98,7 +95,8 @@ const IntegrationForm = observer((props: IntegrationFormProps) => { return ( {
- - - {alertReceiveChannelChoice.display_name} - + + + + {alertReceiveChannelChoice.display_name} + + {alertReceiveChannelChoice.featured && } + {alertReceiveChannelChoice.short_description}
- {alertReceiveChannelChoice.featured && ( - - )} ); }) @@ -133,13 +131,10 @@ const IntegrationForm = observer((props: IntegrationFormProps) => { {(showNewIntegrationForm || id !== 'new') && (
@@ -201,4 +196,4 @@ const IntegrationForm = observer((props: IntegrationFormProps) => { ); }); -export default IntegrationForm; +export default IntegrationForm2; diff --git a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.css b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.css index dc6c6dc9..d51b2bcf 100644 --- a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.css +++ b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.module.css @@ -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; } diff --git a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx index a24eeb6c..e481b102 100644 --- a/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx +++ b/grafana-plugin/src/containers/IntegrationTemplate/IntegrationTemplate.tsx @@ -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(false); const [chatOps, setChatOps] = useState(undefined); @@ -42,6 +45,17 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => { const [changedTemplateBody, setChangedTemplateBody] = useState(templateBody); const [resultError, setResultError] = useState(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 ( - <> - - - - Edit {template.displayName} template - {template.description && {template.description}} - + + + + Edit {template.displayName} template + {template.description && {template.description}} + - - - - + + + -
- } - onClose={onHide} - closeOnMaskClick={false} - width={'95%'} - > + +
+ } + onClose={onHide} + closeOnMaskClick={false} + width={'95%'} + > +
{ value={templateBody} data={undefined} showLineNumbers={true} - height={'100vh'} + height={'85vh'} onChange={getChangeHandler()} />
@@ -203,8 +217,8 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
)} */}
- - +
+ ); }); @@ -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 (
@@ -237,7 +254,7 @@ const Result = (props: ResultProps) => { {error} ) : ( - + { {chatOps && ( {chatOps.comment && ( diff --git a/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx b/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx index deea7efe..4d48e134 100644 --- a/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx +++ b/grafana-plugin/src/containers/TemplatePreview/TemplatePreview.tsx @@ -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') ? ( - + {isCondition ? ( - 'True' + <> + True + ) : ( -
+ + +
+ )} - + ) : (
div { + border-right: none; +} + +.alert-groups-list button { + padding-left: 0; } diff --git a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx index df0fbcdd..c9786efa 100644 --- a/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx +++ b/grafana-plugin/src/containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList.tsx @@ -81,7 +81,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { @@ -101,8 +101,8 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
- - + + {JSON.stringify(selectedAlertPayload, null, 4)} @@ -127,7 +127,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { @@ -136,7 +136,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => { ) : ( <>
- + Recent Alert groups diff --git a/grafana-plugin/src/models/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel.ts index 76f95d0f..ec451819 100644 --- a/grafana-plugin/src/models/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel.ts @@ -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 { diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts index a03ac0cd..cb41f6e5 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.ts @@ -24,9 +24,11 @@ import { export class AlertReceiveChannelStore extends BaseStore { @observable.shallow - // searchResult: { count?: number; results?: Array } = {}; searchResult: Array; + @observable.shallow + paginatedSearchResult: { count?: number; results?: Array } = {}; + @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 { 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', diff --git a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts index 940efc28..62c81c68 100644 --- a/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts +++ b/grafana-plugin/src/models/alert_receive_channel/alert_receive_channel.types.ts @@ -31,6 +31,7 @@ export interface AlertReceiveChannel { is_available_for_integration_heartbeat: boolean; allow_delete: boolean; deleted?: boolean; + routes_count: number; } export interface AlertReceiveChannelOption { diff --git a/grafana-plugin/src/models/channel_filter/channel_filter.types.ts b/grafana-plugin/src/models/channel_filter/channel_filter.types.ts index 1daa4109..ab844e59 100644 --- a/grafana-plugin/src/models/channel_filter/channel_filter.types.ts +++ b/grafana-plugin/src/models/channel_filter/channel_filter.types.ts @@ -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; diff --git a/grafana-plugin/src/pages/index.tsx b/grafana-plugin/src/pages/index.tsx index bb9268e8..934242b2 100644 --- a/grafana-plugin/src/pages/index.tsx +++ b/grafana-plugin/src/pages/index.tsx @@ -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, diff --git a/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx b/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx index d147970a..c38bbe20 100644 --- a/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx @@ -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 = 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
- @@ -229,6 +239,14 @@ const ExpandedIntegrationRouteDisplay: React.FC isDemoModalOpen: false, isEditTemplateModalOpen: false, selectedTemplate: undefined, + isEditRegexpRouteTemplateModalOpen: false, + channelFilterIdForEdit: undefined, + isNewRoute: false, }; } @@ -88,16 +96,28 @@ class Integration2 extends React.Component 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 Routes - @@ -365,12 +385,29 @@ class Integration2 extends React.Component 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 && ( + this.setState({ isEditRegexpRouteTemplateModalOpen: false })} + onUpdateRoute={this.onUpdateRoutesCallback} + onOpenEditIntegrationTemplate={this.openEditTemplateModal} /> )}
@@ -379,6 +416,21 @@ class Integration2 extends React.Component ); } + 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 routeIndex={routeIndex} templates={templates} openEditTemplateModal={this.openEditTemplateModal} + onEditRegexpTemplate={this.handleEditRegexpRouteTemplate} /> ), })); @@ -450,7 +503,11 @@ class Integration2 extends React.Component 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 .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 }); }; + 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 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']) => { diff --git a/grafana-plugin/src/pages/integrations_2/Integrations2.module.scss b/grafana-plugin/src/pages/integrations_2/Integrations2.module.scss index 9deb8cb0..003100ea 100644 --- a/grafana-plugin/src/pages/integrations_2/Integrations2.module.scss +++ b/grafana-plugin/src/pages/integrations_2/Integrations2.module.scss @@ -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; +} diff --git a/grafana-plugin/src/pages/integrations_2/Integrations2.tsx b/grafana-plugin/src/pages/integrations_2/Integrations2.tsx index 9e1885cd..29120675 100644 --- a/grafana-plugin/src/pages/integrations_2/Integrations2.tsx +++ b/grafana-plugin/src/pages/integrations_2/Integrations2.tsx @@ -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 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 <>
- + + Integrations 2
{alertReceiveChannelId && ( - { this.setState({ alertReceiveChannelId: undefined }); }} @@ -239,25 +241,24 @@ class Integrations extends React.Component return ( - - {integration?.display_name} - + {integration?.display_name} ); } renderIntegrationStatus(item: AlertReceiveChannel, alertReceiveChannelStore) { const alertReceiveChannelCounter = alertReceiveChannelStore.counters[item.id]; - let routesCounter = undefined; + let routesCounter = item.routes_count; return ( - + {alertReceiveChannelCounter && ( - /> )} - {routesCounter && } + {routesCounter && ( + + )} ); } @@ -282,20 +291,16 @@ class Integrations extends React.Component const heartbeatStatus = Boolean(heartbeat?.status); return ( -
+
{alertReceiveChannel.is_available_for_integration_heartbeat && ( - -
{}}> - {heartbeatStatus ? : } -
-
+ : } + tooltipTitle={`Last heartbeat: ${heartbeat?.last_heartbeat_time_verbal || 'never'}`} + tooltipContent={undefined} + /> )}
); @@ -311,7 +316,7 @@ class Integrations extends React.Component 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} />
@@ -321,12 +326,6 @@ class Integrations extends React.Component 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 ; }