diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.module.css b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.module.css index 6c4f6366..43b4e87e 100644 --- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.module.css +++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.module.css @@ -49,3 +49,11 @@ .payloadExample { margin-top: 24px; } + +.autoresolve-condition section { + border: 1px solid var(--primary-text-link); +} + +.autoresolve-label { + margin-bottom: 0 !important; +} diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx index 18191c14..ac749fa5 100644 --- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx +++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; +import { getLocationSrv } from '@grafana/runtime'; import { Label, Button, HorizontalGroup, VerticalGroup, Select, LoadingPlaceholder } from '@grafana/ui'; import { capitalCase } from 'change-case'; import cn from 'classnames/bind'; @@ -33,6 +34,7 @@ interface AlertTemplatesFormProps { demoAlertEnabled: boolean; handleSendDemoAlertClick: () => void; templatesRefreshing: boolean; + selectedTemplateName?: string; } const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { @@ -45,6 +47,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { demoAlertEnabled, handleSendDemoAlertClick, templatesRefreshing, + selectedTemplateName, } = props; const [tempValues, setTempValues] = useState<{ @@ -117,15 +120,29 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { [groups, activeGroup] ); + const getGroupByTemplateName = (templateName: string) => { + Object.values(groups).find((group) => { + const foundTemplate = group.find((obj: any) => { + if (obj.name == templateName) { + return obj; + } + }); + setActiveGroup(foundTemplate?.group); + }); + }; + const handleChangeActiveGroup = useCallback((group: SelectableValue) => { setActiveGroup(group.value); }, []); useEffect(() => { const groupsArr = Object.keys(groups); - - if (!activeGroup && groupsArr.length) { - setActiveGroup(groupsArr[0]); + if (selectedTemplateName) { + getGroupByTemplateName(selectedTemplateName); + } else { + if (!activeGroup && groupsArr.length) { + setActiveGroup(groupsArr[0]); + } } }, [groups, activeGroup]); @@ -134,6 +151,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { setActiveTemplate(groups[activeGroup][0]); } }, [activeGroup]); + const getTemplatePreviewEditClickHandler = (templateName: string) => { return () => { const template = templatesToRender.find((template) => template.name === templateName); @@ -163,6 +181,9 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { ) : null} ); + const handleGoToTemplateSettingsCllick = () => { + getLocationSrv().update({ partial: true, query: { tab: 'Autoresolve' } }); + }; return (
@@ -203,9 +224,20 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { key={activeTemplate.name} className={cx('template-form', { 'template-form-full': true, + 'autoresolve-condition': selectedTemplateName && activeTemplate.name == 'resolve_condition_template', })} > - + + {activeTemplate.name == 'resolve_condition_template' && ( + + To activate autoresolving change integration + + + )} void; onUpdateTemplates?: () => void; visible?: boolean; + selectedTemplateName?: string; } const AlertTemplatesFormContainer = observer((props: TeamEditContainerProps) => { - const { alertReceiveChannelId, alertGroupId, onUpdateTemplates } = props; + const { alertReceiveChannelId, alertGroupId, onUpdateTemplates, selectedTemplateName } = props; const store = useStore(); @@ -71,6 +72,7 @@ const AlertTemplatesFormContainer = observer((props: TeamEditContainerProps) => demoAlertEnabled={alertReceiveChannel?.demo_alert_enabled} handleSendDemoAlertClick={handleSendDemoAlertClickCallback} templatesRefreshing={templatesRefreshing} + selectedTemplateName={selectedTemplateName} /> ); }); diff --git a/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx b/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx index b6572c23..e0192f1c 100644 --- a/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx +++ b/grafana-plugin/src/containers/IntegrationSettings/IntegrationSettings.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; +import { getLocationSrv, setLocationSrv } from '@grafana/runtime'; import { Drawer, Tab, @@ -47,6 +48,7 @@ interface IntegrationSettingsProps { const IntegrationSettings = observer((props: IntegrationSettingsProps) => { const { id, onHide, onUpdate, onUpdateTemplates, startTab, alertGroupId } = props; const [activeTab, setActiveTab] = useState(startTab || IntegrationSettingsTab.Templates); + const [selectedTemplate, setSelectedTemplate] = useState(''); const store = useStore(); @@ -57,6 +59,7 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => { const getTabClickHandler = useCallback((tab: IntegrationSettingsTab) => { return () => { setActiveTab(tab); + getLocationSrv().update({ partial: true, query: { tab: tab } }); }; }, []); @@ -64,9 +67,19 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => { alertReceiveChannelStore.updateItem(id); }, []); + useEffect(() => { + setActiveTab(startTab || IntegrationSettingsTab.Templates); + getLocationSrv().update({ partial: true, query: { tab: startTab || IntegrationSettingsTab.Templates } }); + }, [startTab]); + const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel); const [expanded, setExpanded] = useState(false); + + const handleSwitchToTemplate = (templateName: string) => { + setSelectedTemplate(templateName); + }; + return ( { onUpdate={onUpdate} onHide={onHide} onUpdateTemplates={onUpdateTemplates} + selectedTemplateName={selectedTemplate} /> )} {activeTab === IntegrationSettingsTab.Heartbeat && ( @@ -155,7 +169,13 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
)} - {activeTab === IntegrationSettingsTab.Autoresolve && } + {activeTab === IntegrationSettingsTab.Autoresolve && ( + + )} {/*{activeTab === IntegrationSettingsTab.LiveLogs && }*/} {activeTab === IntegrationSettingsTab.HowToConnect && (
diff --git a/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.module.css b/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.module.css index 6e0a3dd6..c3710fe4 100644 --- a/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.module.css +++ b/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.module.css @@ -13,7 +13,7 @@ } .team-select { - width: 300px; + width: 520px; } .team-select-actionbuttons { @@ -28,3 +28,15 @@ .confirmation-buttons .save-team-button { margin-left: 8px; } + +.autoresolve-block { + height: 32px; + padding: 4px 8px; + margin-top: 8px; + min-width: 500px; + width: 520px; +} + +.warning-icon-color { + color: var(--warning-text-color); +} diff --git a/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx b/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx index 167e077e..4e9ea1f7 100644 --- a/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx +++ b/grafana-plugin/src/containers/IntegrationSettings/parts/Autoresolve.tsx @@ -1,14 +1,17 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; -import { Alert, Button, Label, Modal, Select } from '@grafana/ui'; +import { getLocationSrv } from '@grafana/runtime'; +import { Alert, Button, Icon, Label, Modal, Select } from '@grafana/ui'; import cn from 'classnames/bind'; import { get } from 'lodash-es'; import Block from 'components/GBlock/Block'; +import PluginLink from 'components/PluginLink/PluginLink'; import Text from 'components/Text/Text'; import GSelect from 'containers/GSelect/GSelect'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { Alert as AlertType } from 'models/alertgroup/alertgroup.types'; import { Team } from 'models/team/team.types'; import { useStore } from 'state/useStore'; import { UserAction } from 'state/userAction'; @@ -20,9 +23,11 @@ const cx = cn.bind(styles); interface AutoresolveProps { alertReceiveChannelId: AlertReceiveChannel['id']; + alertGroupId?: AlertType['pk']; + onSwitchToTemplate?: (templateName: string) => void; } -const Autoresolve = ({ alertReceiveChannelId }: AutoresolveProps) => { +const Autoresolve = ({ alertReceiveChannelId, onSwitchToTemplate, alertGroupId }: AutoresolveProps) => { const store = useStore(); const { alertReceiveChannelStore, grafanaTeamStore, userStore } = store; @@ -35,11 +40,36 @@ const Autoresolve = ({ alertReceiveChannelId }: AutoresolveProps) => { const [autoresolveChanged, setAutoresolveChanged] = useState(false); const [autoresolveValue, setAutoresolveValue] = useState(alertReceiveChannel?.allow_source_based_resolving); const [showErrorOnTeamSelect, setShowErrorOnTeamSelect] = useState(false); + const [autoresolveSelected, setAutoresolveSelected] = useState( + alertReceiveChannel?.allow_source_based_resolving + ); + const [autoresolveConditionInvalid, setAutoresolveConditionInvalid] = useState(false); + + useEffect(() => { + store.alertReceiveChannelStore.updateItem(alertReceiveChannelId); + store.alertReceiveChannelStore.updateTemplates(alertReceiveChannelId, alertGroupId); + }, [alertGroupId, alertReceiveChannelId, store]); + + useEffect(() => { + const autoresolveCondition = get( + store.alertReceiveChannelStore.templates[alertReceiveChannelId], + 'resolve_condition_template' + ); + if (autoresolveCondition == ['invalid template']) { + setAutoresolveConditionInvalid(true); + } + }, [store.alertReceiveChannelStore.templates[alertReceiveChannelId]]); const handleAutoresolveSelected = useCallback( (autoresolveSelectedOption) => { setAutoresolveChanged(true); setAutoresolveValue(autoresolveSelectedOption?.value); + if (autoresolveSelectedOption?.value === 'true') { + setAutoresolveSelected(true); + } + if (autoresolveSelectedOption?.value === 'false') { + setAutoresolveSelected(false); + } }, [autoresolveChanged] ); @@ -84,6 +114,11 @@ const Autoresolve = ({ alertReceiveChannelId }: AutoresolveProps) => { } }; + const handleGoToTemplateSettingsCllick = () => { + getLocationSrv().update({ partial: true, query: { tab: 'Templates' } }); + onSwitchToTemplate('resolve_condition_template'); + }; + return ( <> @@ -124,12 +159,39 @@ const Autoresolve = ({ alertReceiveChannelId }: AutoresolveProps) => { defaultValue={{ value: 'true', label: 'Automatically resolve' }} value={autoresolveValue.toString()} options={[ - { value: 'true', label: 'Automatically resolve' }, + { value: 'true', label: 'Resolve automatically' }, { value: 'false', label: 'Resolve manually' }, ]} />
+ {autoresolveSelected && ( + <> + +
+ + Incident will be automatically resolved when it matches{' '} + + +
+
+ {autoresolveConditionInvalid && ( + +
+ + Autoresolving condition + template is invalid, please{' '} + + +
+
+ )} + + )}