From dc6192fb7c55697d2661ae6667306ff95de84a31 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 15 May 2023 15:53:31 -0400 Subject: [PATCH 01/11] dont enable silk if maintenance mode is enabled (#1941) --- engine/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/settings/base.py b/engine/settings/base.py index c7c00f39..5e265b4b 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -488,7 +488,7 @@ INTERNAL_IPS = ["127.0.0.1"] SELF_IP = os.environ.get("SELF_IP") SILK_PROFILER_ENABLED = getenv_boolean("SILK_PROFILER_ENABLED", default=False) -if SILK_PROFILER_ENABLED: +if SILK_PROFILER_ENABLED and not IS_IN_MAINTENANCE_MODE: SILK_PATH = os.environ.get("SILK_PATH", "silk/") SILKY_INTERCEPT_PERCENT = getenv_integer("SILKY_INTERCEPT_PERCENT", 100) From 4c5c4f20149b1fad9bf49ec4baa7f181719aad30 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 15 May 2023 16:00:59 -0400 Subject: [PATCH 02/11] update silk_profiler_enabled logic (#1942) --- engine/settings/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/engine/settings/base.py b/engine/settings/base.py index 5e265b4b..eb9ed5d9 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -487,8 +487,9 @@ INTERNAL_IPS = ["127.0.0.1"] SELF_IP = os.environ.get("SELF_IP") -SILK_PROFILER_ENABLED = getenv_boolean("SILK_PROFILER_ENABLED", default=False) -if SILK_PROFILER_ENABLED and not IS_IN_MAINTENANCE_MODE: +SILK_PROFILER_ENABLED = getenv_boolean("SILK_PROFILER_ENABLED", default=False) and not IS_IN_MAINTENANCE_MODE + +if SILK_PROFILER_ENABLED: SILK_PATH = os.environ.get("SILK_PATH", "silk/") SILKY_INTERCEPT_PERCENT = getenv_integer("SILKY_INTERCEPT_PERCENT", 100) From 319cc72cdd91afa8db5b44e073a8e207e7b5078c Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Tue, 16 May 2023 14:18:00 +0300 Subject: [PATCH 03/11] Rares/templating settings (#1937) # What this PR does Iteration on templates&grouping --------- Co-authored-by: Yulia Shanyrova --- .../AlertTemplates/AlertTemplatesForm.tsx | 4 +- .../IntegrationCollapsibleTreeView.tsx | 45 +- .../IntegrationInputField.module.scss} | 1 - .../IntegrationInputField.tsx | 67 ++ .../IntegrationMaskedInputField.tsx | 53 -- .../MonacoEditor.tsx} | 57 +- .../jinja2.ts | 0 grafana-plugin/src/components/Tag/Tag.tsx | 7 +- grafana-plugin/src/components/Text/Text.tsx | 21 +- .../AlertTemplatesFormContainer.tsx | 2 +- .../ChannelFilterForm/ChannelFilterForm.tsx | 8 +- .../EditRegexpRouteTemplateModal.tsx | 4 +- .../IntegrationForm2.helpers.ts | 2 +- .../IntegrationForm/IntegrationForm2.tsx | 92 +-- .../IntegrationTemplate.tsx | 19 +- .../MaintenanceForm/MaintenanceForm.tsx | 18 +- .../MobileAppConnection.test.tsx.snap | 28 +- .../__snapshots__/DownloadIcons.test.tsx.snap | 4 +- .../TemplatesAlertGroupsList.module.css | 4 + .../TemplatesAlertGroupsList.tsx | 33 +- .../UserDisplay/UserDisplayWithAvatar.tsx | 2 +- grafana-plugin/src/models/action.ts | 2 +- .../src/models/alert_receive_channel.ts | 37 - .../alert_receive_channel.ts | 16 +- .../alert_receive_channel.types.ts | 28 +- .../src/models/alertgroup/alertgroup.ts | 2 +- grafana-plugin/src/models/channel_filter.ts | 1 + .../CollapsedIntegrationRouteDisplay.tsx | 50 +- .../ExpandedIntegrationRouteDisplay.tsx | 121 ++-- .../integration_2/Integration2.config.ts | 15 +- .../integration_2/Integration2.helper.ts | 4 +- .../integration_2/Integration2.module.scss | 130 +++- .../src/pages/integration_2/Integration2.tsx | 685 ++++++++++++------ .../Integration2HeartbeatForm.module.scss | 0 .../Integration2HeartbeatForm.tsx | 116 +++ .../IntegrationBlock.module.scss | 2 +- .../pages/integration_2/IntegrationBlock.tsx | 13 +- .../IntegrationTemplateBlock.tsx | 19 +- .../IntegrationTemplatesList.tsx | 630 ++++++++-------- .../pages/integrations_2/Integrations2.tsx | 2 +- .../src/pages/maintenance/Maintenance.tsx | 1 + grafana-plugin/src/style/vars.css | 1 + 42 files changed, 1430 insertions(+), 916 deletions(-) rename grafana-plugin/src/components/{IntegrationMaskedInputField/IntegrationMaskedInputField.module.scss => IntegrationInputField/IntegrationInputField.module.scss} (92%) create mode 100644 grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx delete mode 100644 grafana-plugin/src/components/IntegrationMaskedInputField/IntegrationMaskedInputField.tsx rename grafana-plugin/src/components/{MonacoJinja2Editor/MonacoJinja2Editor.tsx => MonacoEditor/MonacoEditor.tsx} (52%) rename grafana-plugin/src/components/{MonacoJinja2Editor => MonacoEditor}/jinja2.ts (100%) delete mode 100644 grafana-plugin/src/models/alert_receive_channel.ts create mode 100644 grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.module.scss create mode 100644 grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.tsx diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx index 7f6637cf..984042a4 100644 --- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx +++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx @@ -9,7 +9,7 @@ import { omit } from 'lodash-es'; import { templatesToRender, Template } from 'components/AlertTemplates/AlertTemplatesForm.config'; import { getLabelFromTemplateName } from 'components/AlertTemplates/AlertTemplatesForm.helper'; import Block from 'components/GBlock/Block'; -import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor'; +import MonacoEditor from 'components/MonacoEditor/MonacoEditor'; import SourceCode from 'components/SourceCode/SourceCode'; import Text from 'components/Text/Text'; import TemplatePreview from 'containers/TemplatePreview/TemplatePreview'; @@ -216,7 +216,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { )} - { + setExpandedList(getStartingExpandedState()); + }, [configElements]); + return (
{configElements.map((item: IntegrationCollapsibleItem | IntegrationCollapsibleItem[], idx) => { @@ -34,7 +40,7 @@ const IntegrationCollapsibleTreeView: React.FC expandOrCollapseAtPos(idx, innerIdx)} - isExpanded={!!expandedList[idx][innerIdx]} + isExpanded={expandedList[idx][innerIdx]} /> )); } @@ -44,7 +50,7 @@ const IntegrationCollapsibleTreeView: React.FC expandOrCollapseAtPos(idx)} - isExpanded={!!expandedList[idx]} + isExpanded={expandedList[idx] as boolean} /> ); })} @@ -65,6 +71,18 @@ const IntegrationCollapsibleTreeView: React.FC { if (!isUndefined(j) && index === i) { @@ -79,15 +97,22 @@ const IntegrationCollapsibleTreeView: React.FC = ({ - item, - isExpanded, - onClick, -}) => { +const IntegrationCollapsibleTreeItem: React.FC<{ + item: IntegrationCollapsibleItem; + isExpanded: boolean; + onClick: () => void; + canHoverIcon?: boolean; +}> = ({ item, isExpanded, onClick, canHoverIcon = true }) => { + const iconOnClickFn = !item.isCollapsible ? undefined : onClick; + return (
- + {canHoverIcon ? ( + + ) : ( + + )}
{item.expandedView} diff --git a/grafana-plugin/src/components/IntegrationMaskedInputField/IntegrationMaskedInputField.module.scss b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.module.scss similarity index 92% rename from grafana-plugin/src/components/IntegrationMaskedInputField/IntegrationMaskedInputField.module.scss rename to grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.module.scss index 78805c4f..c134cce0 100644 --- a/grafana-plugin/src/components/IntegrationMaskedInputField/IntegrationMaskedInputField.module.scss +++ b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.module.scss @@ -2,7 +2,6 @@ position: relative; display: flex; flex-grow: 1; - margin-right: 24px; height: 25px; } diff --git a/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx new file mode 100644 index 00000000..f7087f87 --- /dev/null +++ b/grafana-plugin/src/components/IntegrationInputField/IntegrationInputField.tsx @@ -0,0 +1,67 @@ +import React, { useState } from 'react'; + +import { HorizontalGroup, IconButton, Input } from '@grafana/ui'; +import cn from 'classnames/bind'; +import CopyToClipboard from 'react-copy-to-clipboard'; + +import { openNotification } from 'utils'; + +import styles from './IntegrationInputField.module.scss'; + +interface IntegrationInputFieldProps { + value: string; + isMasked?: boolean; + showEye?: boolean; + showCopy?: boolean; + showExternal?: boolean; + className?: string; +} + +const cx = cn.bind(styles); + +const IntegrationInputField: React.FC = ({ + isMasked = true, + value, + showEye = true, + showCopy = true, + showExternal = true, + className, +}) => { + const [isInputMasked, setIsMasked] = useState(isMasked); + + return ( +
+
{renderInputField()}
+ +
+ + {showEye && } + {showCopy && ( + + + + )} + {showExternal && } + +
+
+ ); + + function renderInputField() { + return ; + } + + function onInputReveal() { + setIsMasked(!isInputMasked); + } + + function onCopy() { + openNotification("Integration's HTTP Endpoint is copied!"); + } + + function onOpen() { + window.open(value, '_blank'); + } +}; + +export default IntegrationInputField; diff --git a/grafana-plugin/src/components/IntegrationMaskedInputField/IntegrationMaskedInputField.tsx b/grafana-plugin/src/components/IntegrationMaskedInputField/IntegrationMaskedInputField.tsx deleted file mode 100644 index c5bce2de..00000000 --- a/grafana-plugin/src/components/IntegrationMaskedInputField/IntegrationMaskedInputField.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useState } from 'react'; - -import { HorizontalGroup, IconButton, Input } from '@grafana/ui'; -import cn from 'classnames/bind'; -import CopyToClipboard from 'react-copy-to-clipboard'; - -import { openNotification } from 'utils'; - -import styles from './IntegrationMaskedInputField.module.scss'; - -interface IntegrationMaskedInputFieldProps { - value: string; -} - -const cx = cn.bind(styles); - -const IntegrationMaskedInputField: React.FC = ({ value }) => { - const [isMasked, setIsMasked] = useState(true); - - return ( -
-
{renderInputField()}
- -
- - - - - - - -
-
- ); - - function renderInputField() { - return ; - } - - function onInputReveal() { - setIsMasked(!isMasked); - } - - function onCopy() { - openNotification("Integration's HTTP Endpoint is copied!"); - } - - function onOpen() { - window.open(value, '_blank'); - } -}; - -export default IntegrationMaskedInputField; diff --git a/grafana-plugin/src/components/MonacoJinja2Editor/MonacoJinja2Editor.tsx b/grafana-plugin/src/components/MonacoEditor/MonacoEditor.tsx similarity index 52% rename from grafana-plugin/src/components/MonacoJinja2Editor/MonacoJinja2Editor.tsx rename to grafana-plugin/src/components/MonacoEditor/MonacoEditor.tsx index 34c2477e..ce16c0d6 100644 --- a/grafana-plugin/src/components/MonacoJinja2Editor/MonacoJinja2Editor.tsx +++ b/grafana-plugin/src/components/MonacoEditor/MonacoEditor.tsx @@ -4,21 +4,29 @@ import { CodeEditor, CodeEditorSuggestionItemKind, LoadingPlaceholder } from '@g import { getPaths } from 'utils'; -import { conf, language } from './jinja2'; +import { conf, language as jinja2Language } from './jinja2'; declare const monaco: any; -interface MonacoJinja2EditorProps { +interface MonacoEditorProps { value: string; disabled?: boolean; height?: string; + focus?: boolean; data: any; showLineNumbers?: boolean; + useAutoCompleteList?: boolean; + language?: MONACO_LANGUAGE; onChange?: (value: string) => void; loading?: boolean; monacoOptions?: any; } +export enum MONACO_LANGUAGE { + json = 'json', + jinja2 = 'jinja2', +} + const PREDEFINED_TERMS = [ 'grafana_oncall_link', 'integration_name', @@ -27,8 +35,20 @@ const PREDEFINED_TERMS = [ 'tojson_pretty', ]; -const MonacoJinja2Editor: FC = (props) => { - const { value, onChange, disabled, data, height, monacoOptions, showLineNumbers = true, loading = false } = props; +const MonacoEditor: FC = (props) => { + const { + value, + onChange, + disabled, + data, + language = MONACO_LANGUAGE.jinja2, + useAutoCompleteList = true, + focus = true, + height = '130px', + monacoOptions, + showLineNumbers = true, + loading = false, + } = props; const autoCompleteList = useCallback( () => @@ -45,13 +65,17 @@ const MonacoJinja2Editor: FC = (props) => { onChange?.(editor.getValue()); }); - editor.focus(); + if (focus) { + editor.focus(); + } - const jinja2Lang = monaco.languages.getLanguages().find((l: { id: string }) => l.id === 'jinja2'); - if (!jinja2Lang) { - monaco.languages.register({ id: 'jinja2' }); - monaco.languages.setLanguageConfiguration('jinja2', conf); - monaco.languages.setMonarchTokensProvider('jinja2', language); + if (language === MONACO_LANGUAGE.jinja2) { + const jinja2Lang = monaco.languages.getLanguages().find((l: { id: string }) => l.id === 'jinja2'); + if (!jinja2Lang) { + monaco.languages.register({ id: 'jinja2' }); + monaco.languages.setLanguageConfiguration('jinja2', conf); + monaco.languages.setMonarchTokensProvider('jinja2', jinja2Language); + } } }, []); @@ -59,6 +83,11 @@ const MonacoJinja2Editor: FC = (props) => { return ; } + const otherProps: any = {}; + if (useAutoCompleteList) { + otherProps.getSuggestions = { autoCompleteList }; + } + return ( = (props) => { readOnly={disabled} showLineNumbers={showLineNumbers} value={value} - language="jinja2" + language={language} width="100%" - height={height ? `${height}` : `130px`} + height={height} onEditorDidMount={handleMount} - getSuggestions={autoCompleteList} + {...otherProps} /> ); }; -export default MonacoJinja2Editor; +export default MonacoEditor; diff --git a/grafana-plugin/src/components/MonacoJinja2Editor/jinja2.ts b/grafana-plugin/src/components/MonacoEditor/jinja2.ts similarity index 100% rename from grafana-plugin/src/components/MonacoJinja2Editor/jinja2.ts rename to grafana-plugin/src/components/MonacoEditor/jinja2.ts diff --git a/grafana-plugin/src/components/Tag/Tag.tsx b/grafana-plugin/src/components/Tag/Tag.tsx index 088842cb..4de3374c 100644 --- a/grafana-plugin/src/components/Tag/Tag.tsx +++ b/grafana-plugin/src/components/Tag/Tag.tsx @@ -7,6 +7,7 @@ import styles from 'components/Tag/Tag.module.css'; interface TagProps { color?: string; className?: string; + border?: string; children?: any; onClick?: (ev) => void; forwardedRef?: React.MutableRefObject; @@ -15,13 +16,17 @@ interface TagProps { const cx = cn.bind(styles); const Tag: FC = (props) => { - const { children, color, className, onClick } = props; + const { children, color, className, border, onClick } = props; const style: React.CSSProperties = {}; if (color) { style.backgroundColor = color; } + if (border) { + style.border = border; + } + return ( {children} diff --git a/grafana-plugin/src/components/Text/Text.tsx b/grafana-plugin/src/components/Text/Text.tsx index ba7664f0..e03ef2c8 100644 --- a/grafana-plugin/src/components/Text/Text.tsx +++ b/grafana-plugin/src/components/Text/Text.tsx @@ -80,14 +80,19 @@ const Text: TextInterface = (props) => { return ( diff --git a/grafana-plugin/src/containers/AlertTemplatesFormContainer/AlertTemplatesFormContainer.tsx b/grafana-plugin/src/containers/AlertTemplatesFormContainer/AlertTemplatesFormContainer.tsx index bff57aff..aa53a2f0 100644 --- a/grafana-plugin/src/containers/AlertTemplatesFormContainer/AlertTemplatesFormContainer.tsx +++ b/grafana-plugin/src/containers/AlertTemplatesFormContainer/AlertTemplatesFormContainer.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { observer } from 'mobx-react'; import AlertTemplatesForm from 'components/AlertTemplates/AlertTemplatesForm'; -import { AlertReceiveChannel } from 'models/alert_receive_channel'; +import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { Alert } from 'models/alertgroup/alertgroup.types'; import { useStore } from 'state/useStore'; import { openErrorNotification, openNotification } from 'utils'; diff --git a/grafana-plugin/src/containers/ChannelFilterForm/ChannelFilterForm.tsx b/grafana-plugin/src/containers/ChannelFilterForm/ChannelFilterForm.tsx index fd211342..b4e641f5 100644 --- a/grafana-plugin/src/containers/ChannelFilterForm/ChannelFilterForm.tsx +++ b/grafana-plugin/src/containers/ChannelFilterForm/ChannelFilterForm.tsx @@ -6,10 +6,10 @@ import { get } from 'lodash-es'; import { observer } from 'mobx-react'; import Block from 'components/GBlock/Block'; -import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor'; +import MonacoEditor from 'components/MonacoEditor/MonacoEditor'; import Text from 'components/Text/Text'; import IncidentMatcher from 'containers/IncidentMatcher/IncidentMatcher'; -import { AlertReceiveChannel } from 'models/alert_receive_channel'; +import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { ChannelFilter, FilteringTermType } from 'models/channel_filter/channel_filter.types'; import { useStore } from 'state/useStore'; import { openErrorNotification } from 'utils'; @@ -129,7 +129,7 @@ const ChannelFilterForm = observer((props: ChannelFilterFormProps) => { } > - { disabled={data?.is_default} error={errors['filtering_term']} > -
- void; onUpdate: () => void; } const IntegrationForm2 = observer((props: IntegrationFormProps) => { - const { id, onHide, onUpdate } = props; + const { id, onHide, onUpdate, isTableView = true } = props; const store = useStore(); @@ -96,7 +99,7 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => { { )} {(showNewIntegrationForm || id !== 'new') && ( - +
- How the integration works} - contentClassName={cx('collapsable-content')} - > - - The integration will generate the following: -
    -
  • Unique URL endpoint for receiving alerts
  • -
  • - Templates to interpret alerts, tailored for Grafana Alerting{' '} -
  • -
  • Grafana Alerting contact point
  • -
  • Grafana Alerting notification
  • -
- What you’ll need to do next: -
    -
  • - Finish connecting Monitoring system using Unique URL that will be provided on the next step{' '} -
  • -
  • - Set up routes that are based on alert content, such as severity, region, and service{' '} -
  • -
  • Connect escalation chains to the routes
  • -
  • - Review templates and personalize according to your requirements -
  • -
-
-
+ {isTableView && ( + How the integration works} + contentClassName={cx('collapsable-content')} + > + + The integration will generate the following: +
    +
  • Unique URL endpoint for receiving alerts
  • +
  • + Templates to interpret alerts, tailored for Grafana Alerting{' '} +
  • +
  • Grafana Alerting contact point
  • +
  • Grafana Alerting notification
  • +
+ What you’ll need to do next: +
    +
  • + Finish connecting Monitoring system using Unique URL that will be provided on the next step{' '} +
  • +
  • + Set up routes that are based on alert content, such as severity, region, and service{' '} +
  • +
  • Connect escalation chains to the routes
  • +
  • + Review templates and personalize according to your requirements +
  • +
+
+
+ )} {id === 'new' ? (
- void; onUpdate: () => void; @@ -29,6 +31,7 @@ interface MaintenanceFormProps { const MaintenanceForm = observer((props: MaintenanceFormProps) => { const { onUpdate, onHide, initialData = {} } = props; + const maintenanceForm = useMemo(() => (initialData.disabled ? cloneDeep(form) : form), [initialData]); const store = useStore(); @@ -50,11 +53,20 @@ const MaintenanceForm = observer((props: MaintenanceFormProps) => { .catch(showApiError); }, []); + if (initialData.disabled) { + const alertReceiveChannelIdField = maintenanceForm.fields.find((f) => f.name === 'alert_receive_channel_id'); + + if (alertReceiveChannelIdField) { + // Integration page requires this field to be preset and disabled, therefore we add extra field `disabled` for the cloned form + alertReceiveChannelIdField.extra.disabled = true; + } + } + return ( - +
- +
-
@@ -102,9 +105,19 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
- - {JSON.stringify(selectedAlertPayload, null, 4)} - +
+ +
@@ -124,12 +137,16 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
-
diff --git a/grafana-plugin/src/containers/UserDisplay/UserDisplayWithAvatar.tsx b/grafana-plugin/src/containers/UserDisplay/UserDisplayWithAvatar.tsx index 1073520e..4d05b2d2 100644 --- a/grafana-plugin/src/containers/UserDisplay/UserDisplayWithAvatar.tsx +++ b/grafana-plugin/src/containers/UserDisplay/UserDisplayWithAvatar.tsx @@ -29,7 +29,7 @@ const UserDisplayWithAvatar = observer(({ id }: UserDisplayProps) => { return ( - {user.email} + {user.email} ); }); diff --git a/grafana-plugin/src/models/action.ts b/grafana-plugin/src/models/action.ts index e8ada731..ea6fc747 100644 --- a/grafana-plugin/src/models/action.ts +++ b/grafana-plugin/src/models/action.ts @@ -1,4 +1,4 @@ -import { AlertReceiveChannel } from './alert_receive_channel'; +import { AlertReceiveChannel } from './alert_receive_channel/alert_receive_channel.types'; export interface ActionDTO { id: string; diff --git a/grafana-plugin/src/models/alert_receive_channel.ts b/grafana-plugin/src/models/alert_receive_channel.ts deleted file mode 100644 index ec451819..00000000 --- a/grafana-plugin/src/models/alert_receive_channel.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; -import { Heartbeat } from 'models/heartbeat/heartbeat.types'; - -import { UserDTO as User } from './user'; - -export enum MaintenanceMode { - Debug, - Maintenance, -} - -export interface AlertReceiveChannel { - id: string; - integration: string; - smile_code: string; - verbal_name: string; - description: string; - description_short: string; - author: User['pk']; - team: GrafanaTeam['id']; - created_at: string; - integration_url: string; - allow_source_based_resolving: boolean; - is_able_to_autoresolve: boolean; - default_channel_filter: number; - instructions: string; - demo_alert_enabled: boolean; - maintenance_mode?: MaintenanceMode; - maintenance_till?: number; - heartbeat: Heartbeat | null; - is_available_for_integration_heartbeat: boolean; - routes_count: number; -} - -export interface AlertReceiveChannelChoice { - display_name: string; - value: number; -} 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 cb41f6e5..688dba43 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 @@ -251,7 +251,7 @@ export class AlertReceiveChannelStore extends BaseStore { }; if (isOverwrite) { - // This is needed because on Move Up/Down/Removal the store no longer reflects correct state + // This is needed because on Move Up/Down/Removal the store no longer reflects the correct state this.channelFilters = { ...channelFilters, }; @@ -439,8 +439,18 @@ export class AlertReceiveChannelStore extends BaseStore { }); } - async sendDemoAlert(id: AlertReceiveChannel['id']) { - await makeRequest(`${this.path}${id}/send_demo_alert/`, { method: 'POST' }).catch(showApiError); + async sendDemoAlert(id: AlertReceiveChannel['id'], payload: string = undefined) { + const requestConfig: any = { + method: 'POST', + }; + + if (payload) { + requestConfig.data = { + demo_alert_payload: payload, + }; + } + + await makeRequest(`${this.path}${id}/send_demo_alert/`, requestConfig).catch(showApiError); Mixpanel.track('Send Demo Incident', null); } 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 62c81c68..8ab3eb5f 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 @@ -1,16 +1,28 @@ import { IRMPlanStatus } from 'models/alertgroup/alertgroup.types'; import { GrafanaTeam } from 'models/grafana_team/grafana_team.types'; import { Heartbeat } from 'models/heartbeat/heartbeat.types'; -import { UserDTO as User } from 'models/user'; +import { User } from 'models/user/user.types'; export enum MaintenanceMode { Debug = 0, Maintenance = 1, } +export interface AlertReceiveChannelOption { + display_name: string; + value: number; + featured: boolean; + short_description: string; +} + +export interface AlertReceiveChannelCounters { + alerts_count: number; + alert_groups_count: number; +} + export interface AlertReceiveChannel { id: string; - integration: any; + integration: string; smile_code: string; verbal_name: string; description: string; @@ -25,23 +37,17 @@ export interface AlertReceiveChannel { default_channel_filter: number; instructions: string; demo_alert_enabled: boolean; + demo_alert_payload: any; maintenance_mode?: MaintenanceMode; maintenance_till?: number; heartbeat: Heartbeat | null; is_available_for_integration_heartbeat: boolean; + routes_count: number; allow_delete: boolean; deleted?: boolean; - routes_count: number; } -export interface AlertReceiveChannelOption { +export interface AlertReceiveChannelChoice { display_name: string; value: number; - featured: boolean; - short_description: string; -} - -export interface AlertReceiveChannelCounters { - alerts_count: number; - alert_groups_count: number; } diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 6f547f74..82ebae8f 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -1,7 +1,7 @@ import { action, observable } from 'mobx'; import qs from 'query-string'; -import { AlertReceiveChannel } from 'models/alert_receive_channel'; +import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import BaseStore from 'models/base_store'; import { User } from 'models/user/user.types'; import { makeRequest } from 'network'; diff --git a/grafana-plugin/src/models/channel_filter.ts b/grafana-plugin/src/models/channel_filter.ts index 6dc428e4..0fb6754d 100644 --- a/grafana-plugin/src/models/channel_filter.ts +++ b/grafana-plugin/src/models/channel_filter.ts @@ -8,6 +8,7 @@ export interface ChannelFilter { alert_receive_channel: AlertReceiveChannel['id']; slack_channel_id?: SlackChannel['id']; telegram_channel?: TelegramChannel['id']; + escalation_chain?: string; created_at: string; filtering_term: string; is_default: boolean; diff --git a/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.tsx b/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.tsx index a47cfd17..4ce63f5d 100644 --- a/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.tsx @@ -1,16 +1,15 @@ import React, { useState } from 'react'; -import { ConfirmModal, HorizontalGroup, Icon } from '@grafana/ui'; +import { ConfirmModal, HorizontalGroup, Icon, IconButton } from '@grafana/ui'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import PluginLink from 'components/PluginLink/PluginLink'; -import Tag from 'components/Tag/Tag'; import Text from 'components/Text/Text'; -import { AlertReceiveChannel } from 'models/alert_receive_channel'; +import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; +import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { ChannelFilter } from 'models/channel_filter'; import { useStore } from 'state/useStore'; -import { getVar } from 'utils/DOM'; import styles from './CollapsedIntegrationRouteDisplay.module.scss'; import { RouteButtonsDisplay } from './ExpandedIntegrationRouteDisplay'; @@ -40,16 +39,22 @@ const CollapsedIntegrationRouteDisplay: React.FC - - {IntegrationHelper.getRouteConditionWording(alertReceiveChannelStore.channelFilters, routeIndex)} - + {channelFilter.filtering_term && ( - {IntegrationHelper.truncateLine(channelFilter.filtering_term)} + {IntegrationHelper.truncateLine(channelFilter.filtering_term)} )} @@ -77,15 +82,24 @@ const CollapsedIntegrationRouteDisplay: React.FC Escalate to - - - {escalationChain?.name} - - + {escalationChain?.name && ( + + + {escalationChain?.name} + + + )} + {!escalationChain?.name && ( + + )}
diff --git a/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx b/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx index c38bbe20..e8a55bbe 100644 --- a/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx @@ -5,20 +5,19 @@ import { Button, HorizontalGroup, InlineLabel, VerticalGroup, Icon, Tooltip, Con import cn from 'classnames/bind'; import { observer } from 'mobx-react'; -import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor'; +import MonacoEditor from 'components/MonacoEditor/MonacoEditor'; import PluginLink from 'components/PluginLink/PluginLink'; -import Tag from 'components/Tag/Tag'; import Text from 'components/Text/Text'; +import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; import { ChatOpsConnectors } from 'containers/AlertRules/parts'; import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps'; import GSelect from 'containers/GSelect/GSelect'; import TeamName from 'containers/TeamName/TeamName'; import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; -import { AlertReceiveChannel } from 'models/alert_receive_channel'; +import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { AlertTemplatesDTO } from 'models/alert_templates'; import { ChannelFilter } from 'models/channel_filter/channel_filter.types'; import { useStore } from 'state/useStore'; -import { getVar } from 'utils/DOM'; import { UserActions } from 'utils/authorization'; import styles from './ExpandedIntegrationRouteDisplay.module.scss'; @@ -71,6 +70,11 @@ const ExpandedIntegrationRouteDisplay: React.FC - - {IntegrationHelper.getRouteConditionWording(alertReceiveChannelStore.channelFilters, routeIndex)} - + - {routeIndex !== channelFiltersTotal.length - 1 && ( - - - - Routing Template - -
- -
- -
-
- )} +
+ + + {routeIndex !== channelFiltersTotal.length - 1 && ( @@ -177,23 +183,28 @@ const ExpandedIntegrationRouteDisplay: React.FC + + {channelFilter.escalation_chain && ( + + )} {isEscalationCollapsed && ( @@ -272,11 +283,11 @@ export const RouteButtonsDisplay: React.FC = ({ const channelFiltersTotal = Object.keys(alertReceiveChannelStore.channelFilters); return ( - + {routeIndex > 0 && !channelFilter.is_default && ( - - - - ( -
-
this.openIntegrationSettings(id, closeMenu)} - > - Integration Settings -
- -
this.openHearbeat(id, closeMenu)}> - Hearbeat -
- -
this.openStartMaintenance(id, closeMenu)} - > - Start Maintenance -
- -
- - -
- - Are you sure you want to delete {' '} - integration? - - } - > -
this.deleteIntegration(id, closeMenu)}> -
{ - // work-around to prevent 2 modals showing (withContextMenu and ConfirmModal) - const contextMenuEl = - document.querySelector('#integration-menu-options'); - if (contextMenuEl) { - contextMenuEl.style.display = 'none'; - } - }} - > - Stop Maintenance -
-
-
-
-
-
- )} - > - {({ openMenu }) => } - -
+
@@ -231,16 +196,23 @@ class Integration2 extends React.Component {alertReceiveChannel.description_short} )} + {alertReceiveChannelCounter && ( - + + + )} Type: - + - - {integration?.display_name} - + {integration?.display_name} @@ -295,77 +265,87 @@ class Integration2 extends React.Component expandedView: , }, { + customIcon: 'layer-group', isExpanded: false, - isCollapsible: true, - collapsedView: ( + isCollapsible: false, + canHoverIcon: false, + expandedView: ( - +
+ Templates - - Grouping: - - {IntegrationHelper.truncateLine(templates['grouping_id_template'] || '')} - - +
+ + + Grouping: + + {IntegrationHelper.truncateLine(templates['grouping_id_template'] || '')} + + - - Autoresolve: - - {IntegrationHelper.truncateLine(templates['resolve_condition_template'] || '')} - - + + Autoresolve: + + {IntegrationHelper.truncateLine(templates['resolve_condition_template'] || '')} + + - - Visualisation: - Multiple - - + + Visualisation: + Multiple + + + +
+
+
+
} content={null} /> ), - expandedView: ( - - - - Templates - - -
- } - content={ - - } - /> - ), + collapsedView: undefined, }, { - customIcon: 'plus', + customIcon: 'code-branch', isCollapsible: false, collapsedView: null, + canHoverIcon: false, expandedView: (
- Routes - - - + + Routes + + + + + + {this.state.isAddingRoute && } +
), @@ -374,28 +354,22 @@ class Integration2 extends React.Component ]} /> - this.setState({ isDemoModalOpen: false })} - /> {isEditTemplateModalOpen && ( { this.setState({ isEditTemplateModalOpen: undefined, - isNewRoute: false, }); LocationHelper.update({ template: undefined, routeId: undefined }, 'partial'); }} channelFilterId={channelFilterIdForEdit} onUpdateTemplates={this.onUpdateTemplatesCallback} - onUpdateRoute={isNewRoute ? this.onCreateRoutesCallback : this.onUpdateRoutesCallback} + onUpdateRoute={this.onUpdateRoutesCallback} template={selectedTemplate} templateBody={ selectedTemplate?.name === 'route_template' - ? this.getRoutingTemplate(isNewRoute, channelFilterIdForEdit) + ? this.getRoutingTemplate(channelFilterIdForEdit) : templates[selectedTemplate?.name] } /> @@ -416,19 +390,46 @@ class Integration2 extends React.Component ); } - getRoutingTemplate = (isRouteNew: boolean, channelFilterId: ChannelFilter['id']) => { + getRoutingTemplate = (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; - } + + return alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term; }; + handleAddNewRoute = () => { - this.setState({ isNewRoute: true }); - this.openEditTemplateModal('route_template'); + const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store; + const { + params: { id }, + } = this.props.match; + + this.setState( + { + isAddingRoute: true, + }, + () => { + alertReceiveChannelStore + .createChannelFilter({ + order: 0, + alert_receive_channel: id, + filtering_term: NEW_ROUTE_DEFAULT, + filtering_term_type: 1, // non-regex + }) + .then(async (channelFilter: ChannelFilter) => { + this.setState({ isAddingRoute: false, newRoutes: this.state.newRoutes.concat(channelFilter.id) }); + await alertReceiveChannelStore.updateChannelFilters(id, true); + await escalationPolicyStore.updateEscalationPolicies(channelFilter.escalation_chain); + openNotification('A new route has been added'); + }) + .catch((err) => { + const errors = get(err, 'response.data'); + if (errors?.non_field_errors) { + openErrorNotification(errors.non_field_errors); + } + }); + } + ); }; renderRoutesFn = (): IntegrationCollapsibleItem[] => { @@ -442,27 +443,37 @@ class Integration2 extends React.Component const templates = alertReceiveChannelStore.templates[id]; const channelFilterIds = alertReceiveChannelStore.channelFilterIds[id]; - return channelFilterIds.map((channelFilterId: ChannelFilter['id'], routeIndex: number) => ({ - isCollapsible: true, - isExpanded: false, - collapsedView: ( - - ), - expandedView: ( - - ), - })); + return channelFilterIds.map( + (channelFilterId: ChannelFilter['id'], routeIndex: number) => + ({ + isCollapsible: true, + // this will keep new routes expanded at the very first time + isExpanded: this.state.newRoutes.indexOf(channelFilterId) > -1 ? true : false, + onStateChange: () => { + if (this.state.newRoutes.indexOf(channelFilterId) > -1) { + // this will close them on user action + this.setState((prevState) => ({ newRoutes: prevState.newRoutes.filter((r) => r !== channelFilterId) })); + } + }, + collapsedView: ( + + ), + expandedView: ( + + ), + } as IntegrationCollapsibleItem) + ); }; renderHearbeat = (alertReceiveChannel: AlertReceiveChannel) => { @@ -473,6 +484,10 @@ class Integration2 extends React.Component const heartbeatStatus = Boolean(heartbeat?.status); + if (!alertReceiveChannel.heartbeat) { + return null; + } + return ( this.setState({ isEditRegexpRouteTemplateModalOpen: true, channelFilterIdForEdit: channelFilterId }); }; - onCreateRoutesCallback = ({ route_template }: { route_template: string }) => { - const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store; - const { - params: { id }, - } = this.props.match; - - alertReceiveChannelStore - .createChannelFilter({ - order: 0, - alert_receive_channel: id, - filtering_term: route_template, - - // TODO: need to figure out this value - filtering_term_type: 1, - }) - .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); - } - }); - }; - onUpdateRoutesCallback = ( { route_template }: { route_template: string }, - channelFilterId, + channelFilterId: ChannelFilter['id'], filteringTermType?: number ) => { const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store; @@ -549,13 +535,10 @@ class Integration2 extends React.Component 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); }); }) @@ -592,8 +575,10 @@ class Integration2 extends React.Component getTemplatesList = (): CascaderOption[] => INTEGRATION_TEMPLATES_LIST; openEditTemplateModal = (templateName, channelFilterId?: ChannelFilter['id']) => { - this.setState({ selectedTemplate: templateForEdit[templateName] }); - this.setState({ isEditTemplateModalOpen: true }); + this.setState({ + isEditTemplateModalOpen: true, + selectedTemplate: templateForEdit[templateName], + }); if (channelFilterId) { this.setState({ channelFilterIdForEdit: channelFilterId }); @@ -609,14 +594,6 @@ class Integration2 extends React.Component alertReceiveChannelStore.deleteAlertReceiveChannel(id).then(() => history.push(`${PLUGIN_ROOT}/integrations_2/`)); }; - deleteIntegration = (_id: AlertReceiveChannel['id'], _closeMenu: () => void) => {}; - - openIntegrationSettings = (_id: AlertReceiveChannel['id'], _closeMenu: () => void) => {}; - - openStartMaintenance = (_id: AlertReceiveChannel['id'], _closeMenu: () => void) => {}; - - openHearbeat = (_id: AlertReceiveChannel['id'], _closeMenu: () => void) => {}; - async loadIntegration() { const { store: { alertReceiveChannelStore }, @@ -687,9 +664,14 @@ const IntegrationSendDemoPayloadModal: React.FC { const { alertReceiveChannelStore } = useStore(); + const [demoPayload, setDemoPayload] = useState( + JSON.stringify(alertReceiveChannel.demo_alert_payload, null, '\t') + ); + let onPayloadChangeDebounced = debounce(100, onPayloadChange); return ( Alert Payload - + + A demo alert will be generated. You can find it on the Alert Groups page + + } + placement={'top-start'} + > - {getDemoAlertJSON()} +
+ +
+ + + ( +
+
openIntegrationSettings()}> + Integration Settings +
+ +
setIsHearbeatFormOpen(true)}> + Hearbeat +
+ + {!alertReceiveChannel.maintenance_till && ( + +
+ Start Maintenance +
+
+ )} + + {alertReceiveChannel.maintenance_till && ( + +
+
{ + setConfirmModal({ + isOpen: true, + confirmText: 'Stop', + dismissText: 'Cancel', + onConfirm: onStopMaintenance, + title: ( + <> + Are you sure you want to stop the maintenance for{' '} + ? + + ), + }); + }} + > + Stop Maintenance +
+
+
+ )} + +
+ + +
+
{ + setConfirmModal({ + isOpen: true, + title: ( + <> + Are you sure you want to delete {' '} + integration? + + ), + body: <>This action cannot be undone., + onConfirm: deleteIntegration, + dismissText: 'Cancel', + confirmText: 'Delete', + }); + }} + > + + + + Delete Integration + + +
+
+
+
+ )} + > + {({ openMenu }) => } + +
+ + ); + + function deleteIntegration() { + alertReceiveChannelStore + .deleteAlertReceiveChannel(alertReceiveChannel.id) + .then(() => history.push(`${PLUGIN_ROOT}/integrations_2`)); } - function getDemoAlertJSON() { - return JSON.stringify(INTEGRATION_DEMO_PAYLOAD, null, 4); + function openIntegrationSettings() { + setIsIntegrationSettingsOpen(true); + } + + function openStartMaintenance() { + setMaintenanceData({ disabled: true, alert_receive_channel_id: alertReceiveChannel.id }); + } + + function onStopMaintenance() { + setConfirmModal(undefined); + + maintenanceStore + .stopMaintenanceMode(MaintenanceType.alert_receive_channel, id) + .then(() => maintenanceStore.updateMaintenances()) + .then(() => alertReceiveChannelStore.updateItem(alertReceiveChannel.id)); } }; @@ -752,14 +958,21 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id hasCollapsedBorder={false} heading={
- + HTTP Endpoint - + - + How to connect @@ -772,15 +985,13 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id /> ); - function openHowToConnect() {} - function renderContent() { return (
{!hasAlerts && ( - + No alerts yet; try to send a demo alert )} diff --git a/grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.module.scss b/grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.module.scss new file mode 100644 index 00000000..e69de29b diff --git a/grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.tsx b/grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.tsx new file mode 100644 index 00000000..d38f9150 --- /dev/null +++ b/grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { Button, Drawer, Field, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui'; +import cn from 'classnames/bind'; +import { observer } from 'mobx-react'; + +import IntegrationInputField from 'components/IntegrationInputField/IntegrationInputField'; +import Text from 'components/Text/Text'; +import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip'; +import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; +import { SelectOption } from 'state/types'; +import { useStore } from 'state/useStore'; +import { withMobXProviderContext } from 'state/withStore'; +import { UserActions } from 'utils/authorization'; + +const cx = cn.bind({}); + +interface Integration2HearbeatFormProps { + alertReceveChannelId: AlertReceiveChannel['id']; + onClose?: () => void; +} + +const Integration2HearbeatForm = observer(({ alertReceveChannelId, onClose }: Integration2HearbeatFormProps) => { + const [interval, setInterval] = useState(undefined); + + const { heartbeatStore, alertReceiveChannelStore } = useStore(); + + const alertReceiveChannel = alertReceiveChannelStore.items[alertReceveChannelId]; + + useEffect(() => { + heartbeatStore.updateTimeoutOptions(); + }, [heartbeatStore]); + + useEffect(() => { + if (alertReceiveChannel.heartbeat) { + setInterval(alertReceiveChannel.heartbeat.timeout_seconds); + } + }, [alertReceiveChannel]); + + const timeoutOptions = heartbeatStore.timeoutOptions; + + return ( + + + + Start maintenance mode when performing scheduled maintenance or updates on the infrastructure, which may + trigger false alarms. + + + +
+ + + ; + return ; } function onInputReveal() { diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.tsx index 28f095d7..d565ee50 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm2.tsx @@ -60,7 +60,6 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => { ? alertReceiveChannelStore .create(data) .then((response) => { - onHide(); history.push(`${PLUGIN_ROOT}/integrations_2/${response.id}`); }) .catch(() => { From e2284ef337c7fac51d7595b108a825ed8160d147 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Wed, 17 May 2023 14:48:45 +0300 Subject: [PATCH 09/11] Fix escalation reordering (#1957) # What this PR does Fixed bug in Escalation Chains where reordering an item crashed the list #1936 --- CHANGELOG.md | 6 ++++++ grafana-plugin/src/components/Policy/EscalationPolicy.tsx | 6 +++++- .../EscalationChainSteps/EscalationChainSteps.tsx | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba8b83a..1f30dc41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Fixed bug in Escalation Chains where reordering an item crashed the list + ## v1.2.23 (2023-05-15) ### Added diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx index 029f4bab..3560932d 100644 --- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx @@ -36,7 +36,11 @@ import styles from './EscalationPolicy.module.css'; const cx = cn.bind(styles); -export interface EscalationPolicyProps { +interface ElementSortableProps { + index: number; +} + +export interface EscalationPolicyProps extends ElementSortableProps { data: EscalationPolicyType; waitDelays?: any[]; isDisabled?: boolean; diff --git a/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx b/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx index 46c28e8b..7af65132 100644 --- a/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx +++ b/grafana-plugin/src/containers/EscalationChainSteps/EscalationChainSteps.tsx @@ -76,6 +76,7 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => { return ( Date: Wed, 17 May 2023 06:56:57 -0600 Subject: [PATCH 10/11] Fix organizations not being deleted by start_cleanup_deleted_organizations (#1950) Organizations that have been deleted outside OnCall were not being cleaned up by this task as expected. - Use PluginAuthToken instead of GCOM token == None to determine if the oncall organization should be matched in GCOM - Fix how delete was being checked for the instance, the previous method does not work. --- engine/apps/grafana_plugin/helpers/client.py | 5 +++-- engine/apps/user_management/sync.py | 9 ++++++--- engine/apps/user_management/tests/test_sync.py | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/engine/apps/grafana_plugin/helpers/client.py b/engine/apps/grafana_plugin/helpers/client.py index 592ccaed..628244da 100644 --- a/engine/apps/grafana_plugin/helpers/client.py +++ b/engine/apps/grafana_plugin/helpers/client.py @@ -247,8 +247,9 @@ class GcomAPIClient(APIClient): return self.api_get(query) def is_stack_deleted(self, stack_id: str) -> bool: - instance_info = self.get_instance_info(stack_id) - return instance_info and instance_info.get("status") == self.STACK_STATUS_DELETED + url = f"instances?includeDeleted=true&id={stack_id}" + instance_infos, _ = self.api_get(url) + return instance_infos["items"] and instance_infos["items"][0].get("status") == self.STACK_STATUS_DELETED def post_active_users(self, body): return self.api_post("app-active-users", body) diff --git a/engine/apps/user_management/sync.py b/engine/apps/user_management/sync.py index 18e694cd..8fe16747 100644 --- a/engine/apps/user_management/sync.py +++ b/engine/apps/user_management/sync.py @@ -1,6 +1,7 @@ import logging from celery.utils.log import get_task_logger +from django.apps import apps from django.conf import settings from django.utils import timezone @@ -126,12 +127,14 @@ def check_grafana_incident_is_enabled(client): def delete_organization_if_needed(organization): # Organization has a manually set API token, it will not be found within GCOM # and would need to be deleted manually. - if organization.gcom_token is None: - logger.info(f"Organization {organization.pk} has no gcom_token. Probably it's needed to delete org manually.") + PluginAuthToken = apps.get_model("auth_token", "PluginAuthToken") + manually_provisioned_token = PluginAuthToken.objects.filter(organization_id=organization.pk).first() + if manually_provisioned_token: + logger.info(f"Organization {organization.pk} has PluginAuthToken. Probably it's needed to delete org manually.") return False # Use common token as organization.gcom_token could be already revoked - client = GcomAPIClient(settings.GRAFANA_COM_API_TOKEN) + client = GcomAPIClient(settings.GRAFANA_COM_ADMIN_API_TOKEN) is_stack_deleted = client.is_stack_deleted(organization.stack_id) if not is_stack_deleted: return False diff --git a/engine/apps/user_management/tests/test_sync.py b/engine/apps/user_management/tests/test_sync.py index 1157cc74..4a1bf05f 100644 --- a/engine/apps/user_management/tests/test_sync.py +++ b/engine/apps/user_management/tests/test_sync.py @@ -335,7 +335,7 @@ def test_duplicate_user_ids(make_organization, make_user_for_organization): def test_cleanup_organization_deleted(make_organization): organization = make_organization(gcom_token="TEST_GCOM_TOKEN") - with patch.object(GcomAPIClient, "get_instance_info", return_value={"status": "deleted"}): + with patch.object(GcomAPIClient, "api_get", return_value=({"items": [{"status": "deleted"}]}, None)): cleanup_organization(organization.id) organization.refresh_from_db() From e0c1e9974aabec3831b38a4c42806164a413f7d4 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Wed, 17 May 2023 13:39:43 -0600 Subject: [PATCH 11/11] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f30dc41..28ad5925 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## v1.2.24 (2023-05-17) ### Fixed