Rares/templating settings (#1937)

# What this PR does

Iteration on templates&grouping

---------

Co-authored-by: Yulia Shanyrova <yulia.shanyrova@grafana.com>
This commit is contained in:
Rares Mardare 2023-05-16 14:18:00 +03:00 committed by GitHub
parent 4c5c4f2014
commit 319cc72cdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1430 additions and 916 deletions

View file

@ -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) => {
</Button>
</Text>
)}
<MonacoJinja2Editor
<MonacoEditor
value={tempValues[activeTemplate.name] ?? (templates[activeTemplate.name] || '')}
disabled={false}
data={templates}

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { IconButton, IconName } from '@grafana/ui';
import { Icon, IconButton, IconName } from '@grafana/ui';
import cn from 'classnames/bind';
import { isArray, isUndefined } from 'lodash-es';
@ -14,6 +14,8 @@ export interface IntegrationCollapsibleItem {
collapsedView: React.ReactNode;
isCollapsible: boolean;
isExpanded?: boolean;
canHoverIcon?: boolean;
onStateChange?(): void;
}
interface IntegrationCollapsibleTreeViewProps {
@ -25,6 +27,10 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
const [expandedList, setExpandedList] = useState(getStartingExpandedState());
useEffect(() => {
setExpandedList(getStartingExpandedState());
}, [configElements]);
return (
<div className={cx('integrationTree__container')}>
{configElements.map((item: IntegrationCollapsibleItem | IntegrationCollapsibleItem[], idx) => {
@ -34,7 +40,7 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
item={it}
key={`${idx}-${innerIdx}`}
onClick={() => expandOrCollapseAtPos(idx, innerIdx)}
isExpanded={!!expandedList[idx][innerIdx]}
isExpanded={expandedList[idx][innerIdx]}
/>
));
}
@ -44,7 +50,7 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
item={item}
key={idx}
onClick={() => expandOrCollapseAtPos(idx)}
isExpanded={!!expandedList[idx]}
isExpanded={expandedList[idx] as boolean}
/>
);
})}
@ -65,6 +71,18 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
}
function expandOrCollapseAtPos(i: number, j: number = undefined) {
if (j) {
let elem = configElements[i] as IntegrationCollapsibleItem[];
if (elem[j].onStateChange) {
elem[j].onStateChange();
}
} else {
let elem = configElements[i] as IntegrationCollapsibleItem;
if (elem.onStateChange) {
elem.onStateChange();
}
}
setExpandedList(
expandedList.map((elem, index) => {
if (!isUndefined(j) && index === i) {
@ -79,15 +97,22 @@ const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewPro
}
};
const IntegrationCollapsibleTreeItem: React.FC<{ item: IntegrationCollapsibleItem; isExpanded: boolean; onClick }> = ({
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 (
<div className={cx('integrationTree__group')}>
<div className={cx('integrationTree__icon')}>
<IconButton name={getIconName()} onClick={!item.isCollapsible ? undefined : onClick} size="lg" />
{canHoverIcon ? (
<IconButton name={getIconName()} onClick={iconOnClickFn} size="lg" />
) : (
<Icon name={getIconName()} onClick={iconOnClickFn} size="lg" />
)}
</div>
<div className={cx('integrationTree__element', { 'integrationTree__element--visible': isExpanded })}>
{item.expandedView}

View file

@ -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<IntegrationInputFieldProps> = ({
isMasked = true,
value,
showEye = true,
showCopy = true,
showExternal = true,
className,
}) => {
const [isInputMasked, setIsMasked] = useState(isMasked);
return (
<div className={cx('root', { [className]: !!className })}>
<div className={cx('input-container')}>{renderInputField()}</div>
<div className={cx('icons')}>
<HorizontalGroup spacing={'xs'}>
{showEye && <IconButton name={'eye'} size={'xs'} onClick={onInputReveal} />}
{showCopy && (
<CopyToClipboard text={value} onCopy={onCopy}>
<IconButton name={'copy'} size={'xs'} />
</CopyToClipboard>
)}
{showExternal && <IconButton name={'external-link-alt'} size={'xs'} onClick={onOpen} />}
</HorizontalGroup>
</div>
</div>
);
function renderInputField() {
return <Input className={cx('input')} value={isInputMasked ? value.replace(/./g, '*') : value} disabled />;
}
function onInputReveal() {
setIsMasked(!isInputMasked);
}
function onCopy() {
openNotification("Integration's HTTP Endpoint is copied!");
}
function onOpen() {
window.open(value, '_blank');
}
};
export default IntegrationInputField;

View file

@ -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<IntegrationMaskedInputFieldProps> = ({ value }) => {
const [isMasked, setIsMasked] = useState(true);
return (
<div className={cx('root')}>
<div className={cx('input-container')}>{renderInputField()}</div>
<div className={cx('icons')}>
<HorizontalGroup spacing={'xs'}>
<IconButton name={'eye'} size={'xs'} onClick={onInputReveal} />
<CopyToClipboard text={value} onCopy={onCopy}>
<IconButton name={'copy'} size={'xs'} />
</CopyToClipboard>
<IconButton name={'external-link-alt'} size={'xs'} onClick={onOpen} />
</HorizontalGroup>
</div>
</div>
);
function renderInputField() {
return <Input className={cx('input')} value={isMasked ? value.replace(/./g, '*') : value} disabled />;
}
function onInputReveal() {
setIsMasked(!isMasked);
}
function onCopy() {
openNotification("Integration's HTTP Endpoint is copied!");
}
function onOpen() {
window.open(value, '_blank');
}
};
export default IntegrationMaskedInputField;

View file

@ -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<MonacoJinja2EditorProps> = (props) => {
const { value, onChange, disabled, data, height, monacoOptions, showLineNumbers = true, loading = false } = props;
const MonacoEditor: FC<MonacoEditorProps> = (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<MonacoJinja2EditorProps> = (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<MonacoJinja2EditorProps> = (props) => {
return <LoadingPlaceholder text="Loading..." />;
}
const otherProps: any = {};
if (useAutoCompleteList) {
otherProps.getSuggestions = { autoCompleteList };
}
return (
<CodeEditor
monacoOptions={monacoOptions}
@ -66,13 +95,13 @@ const MonacoJinja2Editor: FC<MonacoJinja2EditorProps> = (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;

View file

@ -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<HTMLSpanElement>;
@ -15,13 +16,17 @@ interface TagProps {
const cx = cn.bind(styles);
const Tag: FC<TagProps> = (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 (
<span style={style} className={cx('root', className)} onClick={onClick} ref={props.forwardedRef}>
{children}

View file

@ -80,14 +80,19 @@ const Text: TextInterface = (props) => {
return (
<span
onClick={onClick}
className={cx('root', 'text', className, {
[`text--${type}`]: true,
[`text--${size}`]: true,
'text--strong': strong,
'text--underline': underline,
'no-wrap': !wrap,
keyboard,
})}
className={cx(
'root',
'text',
{
[`text--${type}`]: true,
[`text--${size}`]: true,
'text--strong': strong,
'text--underline': underline,
'no-wrap': !wrap,
keyboard,
},
className
)}
style={style}
{...rest}
>

View file

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

View file

@ -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) => {
</>
}
>
<MonacoJinja2Editor
<MonacoEditor
value={filteringTerm}
disabled={false}
onChange={handleFilteringTermChange}
@ -162,7 +162,7 @@ const ChannelFilterForm = observer((props: ChannelFilterFormProps) => {
disabled={data?.is_default}
error={errors['filtering_term']}
>
<MonacoJinja2Editor
<MonacoEditor
value={filteringTerm}
disabled={false}
onChange={handleFilteringTermChange}

View file

@ -7,7 +7,7 @@ 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 MonacoEditor from 'components/MonacoEditor/MonacoEditor';
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';
@ -86,7 +86,7 @@ const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemplateMod
</HorizontalGroup>
<div className={cx('regexp-template-code')}>
<MonacoJinja2Editor
<MonacoEditor
value={regexpTemplateBody}
height={'200px'}
data={undefined}

View file

@ -1,4 +1,4 @@
import { AlertReceiveChannel } from 'models/alert_receive_channel';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
export function prepareForEdit(item: AlertReceiveChannel) {
return {

View file

@ -10,8 +10,10 @@ import GForm from 'components/GForm/GForm';
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
import Text from 'components/Text/Text';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { AlertReceiveChannel } from 'models/alert_receive_channel';
import { AlertReceiveChannelOption } from 'models/alert_receive_channel/alert_receive_channel.types';
import {
AlertReceiveChannel,
AlertReceiveChannelOption,
} from 'models/alert_receive_channel/alert_receive_channel.types';
import { useStore } from 'state/useStore';
import { UserActions } from 'utils/authorization';
@ -24,12 +26,13 @@ const cx = cn.bind(styles);
interface IntegrationFormProps {
id: AlertReceiveChannel['id'] | 'new';
isTableView?: boolean;
onHide: () => 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) => {
<Block
bordered
hover
withBackground
shadowed
onClick={handleNewIntegrationOptionSelectCallback(alertReceiveChannelChoice)}
key={alertReceiveChannelChoice.value}
className={cx('card', { card_featured: alertReceiveChannelChoice.featured })}
@ -129,48 +132,44 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
</Drawer>
)}
{(showNewIntegrationForm || id !== 'new') && (
<Drawer
scrollableContent
title={id === 'new' ? `New ${selectedOption?.display_name} integration` : `Edit integration`}
onClose={onHide}
closeOnMaskClick={false}
width="640px"
>
<Drawer scrollableContent title={getTitle()} onClose={onHide} closeOnMaskClick={false} width="640px">
<div className={cx('content')}>
<VerticalGroup>
<GForm form={form} data={data} onSubmit={handleSubmit} />
<Collapse
headerWithBackground
className={cx('collapse')}
isOpen={false}
label={<Text type="link">How the integration works</Text>}
contentClassName={cx('collapsable-content')}
>
<Text type="secondary">
The integration will generate the following:
<ul className={cx('integration-info-list')}>
<li className={cx('integration-info-item')}>Unique URL endpoint for receiving alerts </li>
<li className={cx('integration-info-item')}>
Templates to interpret alerts, tailored for Grafana Alerting{' '}
</li>
<li className={cx('integration-info-item')}>Grafana Alerting contact point </li>
<li className={cx('integration-info-item')}>Grafana Alerting notification</li>
</ul>
What youll need to do next:
<ul className={cx('integration-info-list')}>
<li className={cx('integration-info-item')}>
Finish connecting Monitoring system using Unique URL that will be provided on the next step{' '}
</li>
<li className={cx('integration-info-item')}>
Set up routes that are based on alert content, such as severity, region, and service{' '}
</li>
<li className={cx('integration-info-item')}>Connect escalation chains to the routes</li>
<li className={cx('integration-info-item')}>
Review templates and personalize according to your requirements
</li>
</ul>
</Text>
</Collapse>
{isTableView && (
<Collapse
headerWithBackground
className={cx('collapse')}
isOpen={false}
label={<Text type="link">How the integration works</Text>}
contentClassName={cx('collapsable-content')}
>
<Text type="secondary">
The integration will generate the following:
<ul className={cx('integration-info-list')}>
<li className={cx('integration-info-item')}>Unique URL endpoint for receiving alerts </li>
<li className={cx('integration-info-item')}>
Templates to interpret alerts, tailored for Grafana Alerting{' '}
</li>
<li className={cx('integration-info-item')}>Grafana Alerting contact point </li>
<li className={cx('integration-info-item')}>Grafana Alerting notification</li>
</ul>
What youll need to do next:
<ul className={cx('integration-info-list')}>
<li className={cx('integration-info-item')}>
Finish connecting Monitoring system using Unique URL that will be provided on the next step{' '}
</li>
<li className={cx('integration-info-item')}>
Set up routes that are based on alert content, such as severity, region, and service{' '}
</li>
<li className={cx('integration-info-item')}>Connect escalation chains to the routes</li>
<li className={cx('integration-info-item')}>
Review templates and personalize according to your requirements
</li>
</ul>
</Text>
</Collapse>
)}
<HorizontalGroup justify="flex-end">
{id === 'new' ? (
<Button variant="secondary" onClick={() => setShowNewIntegrationForm(false)}>
@ -194,6 +193,13 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
)}
</>
);
function getTitle(): string {
if (!isTableView) {
return 'Integration Settings';
}
return id === 'new' ? `New ${selectedOption?.display_name} integration` : `Edit integration`;
}
});
export default IntegrationForm2;

View file

@ -1,6 +1,6 @@
import React, { useCallback, useState, useEffect } from 'react';
import { Button, HorizontalGroup, VerticalGroup, Icon, Drawer } from '@grafana/ui';
import { Button, HorizontalGroup, Drawer, VerticalGroup, Icon } from '@grafana/ui';
import cn from 'classnames/bind';
import { debounce } from 'lodash-es';
import { observer } from 'mobx-react';
@ -13,7 +13,7 @@ import {
webTitleTemplateCheatSheet,
} from 'components/CheatSheet/CheatSheet.config';
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 TemplatePreview from 'containers/TemplatePreview/TemplatePreview';
import TemplatesAlertGroupsList from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
@ -45,14 +45,11 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
const [changedTemplateBody, setChangedTemplateBody] = useState<string>(templateBody);
const [resultError, setResultError] = useState<string>(undefined);
const locationParams: any = { template: template.name };
if (template.isRoute) {
locationParams.routeId = channelFilterId;
}
LocationHelper.update(locationParams, 'partial');
useEffect(() => {
const locationParams: any = { template: template.name };
if (template.isRoute) {
locationParams.routeId = channelFilterId;
}
LocationHelper.update(locationParams, 'partial');
}, []);
@ -67,7 +64,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
const getChangeHandler = () => {
return debounce((value: string) => {
setChangedTemplateBody(value);
}, 1000);
}, 500);
};
const onEditPayload = (alertPayload: string) => {
@ -188,7 +185,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
</HorizontalGroup>
</div>
<MonacoJinja2Editor
<MonacoEditor
value={templateBody}
data={undefined}
showLineNumbers={true}

View file

@ -1,7 +1,8 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Button, Drawer, HorizontalGroup, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { cloneDeep } from 'lodash-es';
import { observer } from 'mobx-react';
import GForm from 'components/GForm/GForm';
@ -22,6 +23,7 @@ interface MaintenanceFormProps {
initialData: {
type?: MaintenanceType;
alert_receive_channel_id?: AlertReceiveChannel['id'];
disabled?: boolean;
};
onHide: () => 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 (
<Drawer scrollableContent title="Start Maintenance Mode" onClose={onHide} closeOnMaskClick={false}>
<Drawer width="640px" scrollableContent title="Start Maintenance Mode" onClose={onHide} closeOnMaskClick={false}>
<div className={cx('content')}>
<VerticalGroup>
<GForm form={form} data={initialData} onSubmit={handleSubmit} />
<GForm form={maintenanceForm} data={initialData} onSubmit={handleSubmit} />
<HorizontalGroup justify="flex-end">
<Button variant="secondary" onClick={onHide}>
Cancel

View file

@ -55,7 +55,7 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
iOS
</span>
@ -80,7 +80,7 @@ exports[`MobileAppConnection if we disconnect the app, it disconnects and fetche
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
Android
</span>
@ -2400,7 +2400,7 @@ exports[`MobileAppConnection it shows a QR code if the app isn't already connect
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
iOS
</span>
@ -2425,7 +2425,7 @@ exports[`MobileAppConnection it shows a QR code if the app isn't already connect
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
Android
</span>
@ -2513,7 +2513,7 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
iOS
</span>
@ -2538,7 +2538,7 @@ exports[`MobileAppConnection it shows a loading message if it is currently disco
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
Android
</span>
@ -2626,7 +2626,7 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
iOS
</span>
@ -2651,7 +2651,7 @@ exports[`MobileAppConnection it shows a loading message if it is currently fetch
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
Android
</span>
@ -2739,7 +2739,7 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
iOS
</span>
@ -2764,7 +2764,7 @@ exports[`MobileAppConnection it shows a message when the mobile app is already c
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
Android
</span>
@ -2952,7 +2952,7 @@ exports[`MobileAppConnection it shows an error message if there was an error dis
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
iOS
</span>
@ -2977,7 +2977,7 @@ exports[`MobileAppConnection it shows an error message if there was an error dis
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
Android
</span>
@ -3056,7 +3056,7 @@ exports[`MobileAppConnection it shows an error message if there was an error fet
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
iOS
</span>
@ -3081,7 +3081,7 @@ exports[`MobileAppConnection it shows an error message if there was an error fet
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
Android
</span>

View file

@ -49,7 +49,7 @@ exports[`DownloadIcons it renders properly 1`] = `
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
iOS
</span>
@ -74,7 +74,7 @@ exports[`DownloadIcons it renders properly 1`] = `
src="[object Object]"
/>
<span
class="root text icon-text text--primary text--medium"
class="root text text--primary text--medium icon-text"
>
Android
</span>

View file

@ -22,3 +22,7 @@
.alert-groups-list button {
padding-left: 0;
}
.alert-groups-editor {
width: 100%;
}

View file

@ -4,11 +4,11 @@ import { Button, HorizontalGroup, Tooltip, Icon, VerticalGroup, IconButton, Badg
import cn from 'classnames/bind';
import { debounce } from 'lodash-es';
import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor';
import SourceCode from 'components/SourceCode/SourceCode';
import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor';
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 { MONACO_PAYLOAD_OPTIONS } from 'pages/integration_2/Integration2.config';
import { useStore } from 'state/useStore';
import styles from './TemplatesAlertGroupsList.module.css';
@ -78,12 +78,15 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
</HorizontalGroup>
</div>
<div className={cx('alert-groups-list')}>
<MonacoJinja2Editor
<MonacoEditor
value={JSON.stringify(selectedAlertPayload, null, 4)}
data={undefined}
height={'85vh'}
onChange={getChangeHandler()}
showLineNumbers
useAutoCompleteList={false}
language={MONACO_LANGUAGE.json}
monacoOptions={MONACO_PAYLOAD_OPTIONS}
/>
</div>
</>
@ -102,9 +105,19 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
<div className={cx('alert-groups-list')}>
<VerticalGroup>
<Badge color="blue" text="Last alert payload" />
<SourceCode className={cx('alert-group-payload-view')} noMaxHeight showClipboardIconOnly>
{JSON.stringify(selectedAlertPayload, null, 4)}
</SourceCode>
<div className={cx('alert-groups-editor')}>
<MonacoEditor
value={JSON.stringify(selectedAlertPayload, null, 4)}
data={undefined}
disabled
height={'85vh'}
onChange={getChangeHandler()}
showLineNumbers
useAutoCompleteList={false}
language={MONACO_LANGUAGE.json}
monacoOptions={MONACO_PAYLOAD_OPTIONS}
/>
</div>
</VerticalGroup>
</div>
</>
@ -124,12 +137,16 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
</HorizontalGroup>
</div>
<div className={cx('alert-groups-list')}>
<MonacoJinja2Editor
<MonacoEditor
value={null}
disabled={true}
useAutoCompleteList={false}
language={MONACO_LANGUAGE.json}
data={undefined}
monacoOptions={MONACO_PAYLOAD_OPTIONS}
showLineNumbers={false}
height={'85vh'}
onChange={getChangeHandler()}
showLineNumbers
/>
</div>
</>

View file

@ -29,7 +29,7 @@ const UserDisplayWithAvatar = observer(({ id }: UserDisplayProps) => {
return (
<HorizontalGroup spacing="xs">
<Avatar size="small" src={user.avatar}></Avatar>
<Text type="secondary">{user.email}</Text>
<Text type="primary">{user.email}</Text>
</HorizontalGroup>
);
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<CollapsedIntegrationRouteDispla
return (
<>
<IntegrationBlock
hasCollapsedBorder
hasCollapsedBorder={false}
key={channelFilterId}
heading={
<HorizontalGroup justify={'space-between'}>
<HorizontalGroup spacing={'md'}>
<Tag color={getVar('--tag-primary')}>
{IntegrationHelper.getRouteConditionWording(alertReceiveChannelStore.channelFilters, routeIndex)}
</Tag>
<TooltipBadge
borderType="success"
text={IntegrationHelper.getRouteConditionWording(
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
routeIndex
)}
tooltipTitle={undefined}
tooltipContent={undefined}
/>
{channelFilter.filtering_term && (
<Text type="link">{IntegrationHelper.truncateLine(channelFilter.filtering_term)}</Text>
<Text type="primary">{IntegrationHelper.truncateLine(channelFilter.filtering_term)}</Text>
)}
</HorizontalGroup>
<HorizontalGroup>
@ -77,15 +82,24 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
<HorizontalGroup>
<Icon name="list-ui-alt" />
<Text type="secondary">Escalate to</Text>
<PluginLink
className={cx('hover-button')}
target="_blank"
query={{ page: 'escalations', id: channelFilter.escalation_chain }}
>
<Text type="primary" strong>
{escalationChain?.name}
</Text>
</PluginLink>
{escalationChain?.name && (
<PluginLink
className={cx('hover-button')}
target="_blank"
query={{ page: 'escalations', id: channelFilter.escalation_chain }}
>
<Text type="primary" strong>
{escalationChain?.name}
</Text>
</PluginLink>
)}
{!escalationChain?.name && (
<IconButton
name="info-circle"
tooltip={'You have no selected escalation chain for this route'}
size={'md'}
/>
)}
</HorizontalGroup>
</HorizontalGroup>
</div>

View file

@ -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<ExpandedIntegrationRouteDisplayP
return null;
}
const escalationChainRedirectObj: any = { page: 'escalations' };
if (channelFilter.escalation_chain) {
escalationChainRedirectObj.id = channelFilter.escalation_chain;
}
return (
<>
<IntegrationBlock
@ -79,9 +83,15 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
heading={
<HorizontalGroup justify={'space-between'}>
<HorizontalGroup spacing={'md'}>
<Tag color={getVar('--tag-primary')}>
{IntegrationHelper.getRouteConditionWording(alertReceiveChannelStore.channelFilters, routeIndex)}
</Tag>
<TooltipBadge
borderType="success"
text={IntegrationHelper.getRouteConditionWording(
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
routeIndex
)}
tooltipTitle={undefined}
tooltipContent={undefined}
/>
</HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
<RouteButtonsDisplay
@ -95,35 +105,31 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
}
content={
<VerticalGroup spacing="xs">
{routeIndex !== channelFiltersTotal.length - 1 && (
<IntegrationBlockItem>
<HorizontalGroup spacing="xs">
<InlineLabel width={20} tooltip={'TODO: Add text'}>
Routing Template
</InlineLabel>
<div className={cx('input', 'input--short')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(channelFilter.filtering_term, false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
<Button
variant={'secondary'}
icon="edit"
size={'md'}
onClick={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
<IntegrationBlockItem>
<HorizontalGroup spacing="xs">
<InlineLabel width={20}>Routing Template</InlineLabel>
<div className={cx('input', 'input--short')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(channelFilter.filtering_term, false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
<Button variant="secondary" size="md" onClick={undefined}>
<Text type="link">Help</Text>
<Icon name="angle-down" size="sm" />
</Button>
</HorizontalGroup>
</IntegrationBlockItem>
)}
</div>
<Button
variant={'secondary'}
icon="edit"
size={'md'}
onClick={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
/>
<Button variant="secondary" size="md" onClick={undefined}>
<Text type="link">Help</Text>
<Icon name="angle-down" size="sm" />
</Button>
</HorizontalGroup>
</IntegrationBlockItem>
{routeIndex !== channelFiltersTotal.length - 1 && (
<IntegrationBlockItem>
@ -177,23 +183,28 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
</WithPermissionControlTooltip>
<Button variant={'secondary'} icon={'sync'} size={'md'} onClick={onEscalationChainsRefresh} />
<PluginLink
className={cx('hover-button')}
target="_blank"
query={{ page: 'escalations', id: channelFilter.escalation_chain }}
>
<Button variant={'secondary'} icon={'external-link-alt'} size={'md'} />
<PluginLink className={cx('hover-button')} target="_blank" query={escalationChainRedirectObj}>
<Button
variant={'secondary'}
tooltip={channelFilter.escalation_chain ? 'Edit escalation chain' : 'Add escalation chain'}
icon={'external-link-alt'}
size={'md'}
/>
</PluginLink>
<Button
variant={'secondary'}
onClick={() => setState({ isEscalationCollapsed: !isEscalationCollapsed })}
>
<HorizontalGroup>
<Text type="link">Show escalation chain</Text>
{isEscalationCollapsed && <Icon name={'angle-right'} />}
{!isEscalationCollapsed && <Icon name={'angle-up'} />}
</HorizontalGroup>
</Button>
{channelFilter.escalation_chain && (
<Button
variant={'secondary'}
onClick={() => setState({ isEscalationCollapsed: !isEscalationCollapsed })}
>
<HorizontalGroup>
<Text type="link">{isEscalationCollapsed ? 'Show' : 'Hide'} escalation chain</Text>
{isEscalationCollapsed && <Icon name={'angle-right'} />}
{!isEscalationCollapsed && <Icon name={'angle-up'} />}
</HorizontalGroup>
</Button>
)}
</HorizontalGroup>
{isEscalationCollapsed && (
@ -272,11 +283,11 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
const channelFiltersTotal = Object.keys(alertReceiveChannelStore.channelFilters);
return (
<HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
{routeIndex > 0 && !channelFilter.is_default && (
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Tooltip placement="top" content={'Move Up'}>
<Button variant={'secondary'} onClick={onRouteMoveUp} icon={'arrow-up'} size={'xs'} />
<Button variant={'secondary'} onClick={onRouteMoveUp} icon={'arrow-up'} size={'sm'} />
</Tooltip>
</WithPermissionControlTooltip>
)}
@ -284,7 +295,7 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
{routeIndex < channelFiltersTotal.length - 2 && !channelFilter.is_default && (
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Tooltip placement="top" content={'Move Down'}>
<Button variant={'secondary'} onClick={onRouteMoveDown} icon={'arrow-down'} size={'xs'} />
<Button variant={'secondary'} onClick={onRouteMoveDown} icon={'arrow-down'} size={'sm'} />
</Tooltip>
</WithPermissionControlTooltip>
)}
@ -292,7 +303,7 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
{!channelFilter.is_default && (
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Tooltip placement="top" content={'Delete'}>
<Button variant={'secondary'} icon={'trash-alt'} size={'xs'} onClick={setRouteIdForDeletion} />
<Button variant={'secondary'} icon={'trash-alt'} size={'sm'} onClick={setRouteIdForDeletion} />
</Tooltip>
</WithPermissionControlTooltip>
)}

View file

@ -19,13 +19,14 @@ export const MONACO_OPTIONS = {
},
};
export const INTEGRATION_DEMO_PAYLOAD = {
alert_uid: '08d6891a-835c-e661-39fa-96b6a9e26552',
title: 'The whole system is down',
image_url: 'https://http.cat/500',
state: 'alerting',
link_to_upstream_details: 'https://en.wikipedia.org/wiki/Downtime',
message: 'Smth happened. Oh no!',
export const MONACO_PAYLOAD_OPTIONS = {
renderLineHighlight: false,
readOnly: false,
hideCursorInOverviewRuler: true,
minimap: { enabled: false },
cursorStyle: {
display: 'none',
},
};
export const MONACO_INPUT_HEIGHT_SMALL = '32px';

View file

@ -1,6 +1,6 @@
import dayjs from 'dayjs';
import { MaintenanceMode } from 'models/alert_receive_channel';
import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
import { ChannelFilter } from 'models/channel_filter';
import { MAX_CHARACTERS_COUNT, TEXTAREA_ROWS_COUNT } from './Integration2.config';
@ -31,7 +31,7 @@ const IntegrationHelper = {
return slice.length === line.length ? slice : `${slice} ...`;
},
getRouteConditionWording(channelFilters: { [id: string]: ChannelFilter }, routeIndex: number) {
getRouteConditionWording(channelFilters: Array<ChannelFilter['id']>, routeIndex: number) {
const totalCount = Object.keys(channelFilters).length;
if (routeIndex === totalCount - 1) {

View file

@ -1,35 +1,69 @@
$FLEX-GAP: 4px;
$MARGIN: 12px;
$ITEMS-MARGIN: 24px;
$LARGE-MARGIN: 24px;
.integration__heading-container,
.integration__subheading-container {
margin-bottom: $ITEMS-MARGIN;
}
.integration {
&__heading-container {
margin-bottom: calc($LARGE-MARGIN - 12px);
}
.integration__heading-container {
display: flex;
gap: $FLEX-GAP;
}
&__subheading-container {
margin-bottom: $LARGE-MARGIN;
}
.integration__heading {
display: flex;
justify-content: flex-end;
flex-direction: row;
width: 100%;
}
&__payloadInput {
width: 100%;
}
.integration__actions {
display: flex;
gap: $FLEX-GAP;
margin-left: auto;
}
&__alertsPanel {
padding-bottom: 12px;
}
.integration__actionsList {
display: flex;
flex-direction: column;
width: 160px;
border-radius: 2px;
&__name {
margin-bottom: 0;
}
&__heading-container {
display: flex;
gap: $FLEX-GAP;
}
&__heading {
display: flex;
justify-content: flex-end;
flex-direction: row;
width: 100%;
}
&__actions {
display: flex;
gap: $FLEX-GAP;
margin-left: auto;
}
&__actionsList {
display: flex;
flex-direction: column;
width: 160px;
border-radius: 2px;
}
&__description {
display: block;
margin-bottom: $LARGE-MARGIN;
}
&__counter {
font-size: 12px;
}
&__countersBadge {
line-height: 16px;
padding: 3px 4px;
}
&__input-field {
margin-right: 24px;
}
}
.integration__actionItem {
@ -51,11 +85,6 @@ $ITEMS-MARGIN: 24px;
}
}
.integration__description {
display: block;
margin-bottom: $MARGIN;
}
.hamburger-menu {
display: inline-flex;
flex-direction: column;
@ -72,16 +101,10 @@ $ITEMS-MARGIN: 24px;
color: var(--primary-text-color);
}
.integration__counter {
font-size: 12px;
}
.integration__countersBadge {
line-height: 16px;
padding: 3px 4px;
}
.loadingPlaceholder {
margin-bottom: 0;
margin-right: 4px;
animation: none;
}
.customise-button button {
@ -90,6 +113,14 @@ $ITEMS-MARGIN: 24px;
.routesSection {
padding-top: 20px;
&__heading {
font-size: 16px;
}
&__add {
margin-bottom: $LARGE-MARGIN;
}
}
.input {
@ -112,5 +143,24 @@ $ITEMS-MARGIN: 24px;
}
.heartbeat-badge {
padding: 4px 12px;
padding: 4px 8px;
}
.templates__content {
padding-left: 12px;
}
.templates__content,
.templates__container {
display: flex;
width: 100%;
align-items: center;
}
.templates__edit {
margin-left: 0;
}
.template-drawer {
padding-bottom: 24px;
}

View file

@ -1,4 +1,4 @@
import React, { useRef } from 'react';
import React, { useRef, useState } from 'react';
import {
Button,
@ -10,37 +10,43 @@ import {
Modal,
CascaderOption,
IconButton,
ConfirmModal,
Drawer,
} from '@grafana/ui';
import cn from 'classnames/bind';
import { get } from 'lodash-es';
import { observer } from 'mobx-react';
import CopyToClipboard from 'react-copy-to-clipboard';
import Emoji from 'react-emoji-render';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom';
import { debounce } from 'throttle-debounce';
import { TemplateForEdit, templateForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
import IntegrationCollapsibleTreeView, {
IntegrationCollapsibleItem,
} from 'components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView';
import IntegrationInputField from 'components/IntegrationInputField/IntegrationInputField';
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
import IntegrationMaskedInputField from 'components/IntegrationMaskedInputField/IntegrationMaskedInputField';
import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor';
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import { initErrorDataState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
import PluginLink from 'components/PluginLink/PluginLink';
import SourceCode from 'components/SourceCode/SourceCode';
import Tag from 'components/Tag/Tag';
import Text from 'components/Text/Text';
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
import EditRegexpRouteTemplateModal from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal';
import IntegrationForm2 from 'containers/IntegrationForm/IntegrationForm2';
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
import MaintenanceForm from 'containers/MaintenanceForm/MaintenanceForm';
import TeamName from 'containers/TeamName/TeamName';
import UserDisplayWithAvatar from 'containers/UserDisplay/UserDisplayWithAvatar';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { HeartGreenIcon, HeartRedIcon } from 'icons';
import { AlertReceiveChannel } from 'models/alert_receive_channel';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { ChannelFilter } from 'models/channel_filter';
import { MaintenanceType } from 'models/maintenance/maintenance.types';
import { API_HOST, API_PATH_PREFIX } from 'network';
import { PageProps, WithStoreProps } from 'state/types';
import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore';
@ -52,9 +58,10 @@ import { DATASOURCE_ALERTING, PLUGIN_ROOT } from 'utils/consts';
import CollapsedIntegrationRouteDisplay from './CollapsedIntegrationRouteDisplay';
import ExpandedIntegrationRouteDisplay from './ExpandedIntegrationRouteDisplay';
import { INTEGRATION_DEMO_PAYLOAD, INTEGRATION_TEMPLATES_LIST } from './Integration2.config';
import { INTEGRATION_TEMPLATES_LIST, MONACO_PAYLOAD_OPTIONS } from './Integration2.config';
import IntegrationHelper from './Integration2.helper';
import styles from './Integration2.module.scss';
import Integration2HeartbeatForm from './Integration2HeartbeatForm';
import IntegrationBlock from './IntegrationBlock';
import IntegrationTemplateList from './IntegrationTemplatesList';
@ -68,12 +75,14 @@ interface Integration2State extends PageBaseState {
selectedTemplate: TemplateForEdit;
isEditRegexpRouteTemplateModalOpen: boolean;
channelFilterIdForEdit: ChannelFilter['id'];
isNewRoute: boolean;
isTemplateSettingsOpen: boolean;
newRoutes: string[];
isAddingRoute: boolean;
}
// This can be further improved by using a ref instead
const ACTIONS_LIST_WIDTH = 160;
const ACTIONS_LIST_BORDER = 2;
const NEW_ROUTE_DEFAULT = '{{ (payload.severity == "foo" and "bar" in payload.region) or True }}';
@observer
class Integration2 extends React.Component<Integration2Props, Integration2State> {
@ -87,7 +96,9 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
selectedTemplate: undefined,
isEditRegexpRouteTemplateModalOpen: false,
channelFilterIdForEdit: undefined,
isNewRoute: false,
isTemplateSettingsOpen: false,
newRoutes: [],
isAddingRoute: false,
};
}
@ -111,12 +122,11 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
render() {
const {
errorData,
isDemoModalOpen,
isEditTemplateModalOpen,
selectedTemplate,
isEditRegexpRouteTemplateModalOpen,
channelFilterIdForEdit,
isNewRoute,
isTemplateSettingsOpen,
} = this.state;
const {
store: { alertReceiveChannelStore, grafanaTeamStore },
@ -146,6 +156,29 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
<PageErrorHandlingWrapper errorData={errorData} objectName="integration" pageName="Integration">
{() => (
<div className={cx('root')}>
{isTemplateSettingsOpen && (
<Drawer
width="75%"
scrollableContent
title="Template Settings"
onClose={() => this.setState({ isTemplateSettingsOpen: false })}
closeOnMaskClick={false}
>
<IntegrationBlock
className={cx('template-drawer')}
hasCollapsedBorder
heading={undefined}
content={
<IntegrationTemplateList
alertReceiveChannelId={alertReceiveChannel.id}
openEditTemplateModal={this.openEditTemplateModal}
templates={templates}
/>
}
/>
</Drawer>
)}
<div className={cx('integration__heading-container')}>
<PluginLink query={{ page: 'integrations_2' }}>
<IconButton name="arrow-left" size="xxl" />
@ -154,75 +187,7 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
<Emoji text={alertReceiveChannel.verbal_name} />
</h1>
<div className={cx('integration__actions')}>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsTest}>
<Button
variant="secondary"
size="md"
onClick={() => this.setState({ isDemoModalOpen: true })}
data-testid="send-demo-alert"
>
Send demo alert
</Button>
</WithPermissionControlTooltip>
<WithContextMenu
renderMenuItems={({ closeMenu }) => (
<div className={cx('integration__actionsList')} id="integration-menu-options">
<div
className={cx('integration__actionItem')}
onClick={() => this.openIntegrationSettings(id, closeMenu)}
>
<Text type="primary">Integration Settings</Text>
</div>
<div className={cx('integration__actionItem')} onClick={() => this.openHearbeat(id, closeMenu)}>
Hearbeat
</div>
<div
className={cx('integration__actionItem')}
onClick={() => this.openStartMaintenance(id, closeMenu)}
>
<Text type="primary">Start Maintenance</Text>
</div>
<div className="thin-line-break" />
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<div className={cx('integration__actionItem')}>
<WithConfirm
title="Delete integration?"
body={
<>
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} />{' '}
integration?
</>
}
>
<div onClick={() => this.deleteIntegration(id, closeMenu)}>
<div
onClick={() => {
// work-around to prevent 2 modals showing (withContextMenu and ConfirmModal)
const contextMenuEl =
document.querySelector<HTMLElement>('#integration-menu-options');
if (contextMenuEl) {
contextMenuEl.style.display = 'none';
}
}}
>
<Text type="danger">Stop Maintenance</Text>
</div>
</div>
</WithConfirm>
</div>
</WithPermissionControlTooltip>
</div>
)}
>
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} />}
</WithContextMenu>
</div>
<IntegrationActions alertReceiveChannel={alertReceiveChannel} />
</div>
<div className={cx('integration__subheading-container')}>
@ -231,16 +196,23 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
{alertReceiveChannel.description_short}
</Text>
)}
<HorizontalGroup>
{alertReceiveChannelCounter && (
<TooltipBadge
borderType="primary"
tooltipTitle={undefined}
tooltipContent={this.getAlertReceiveChannelCounterTooltip()}
text={
alertReceiveChannelCounter?.alerts_count + '/' + alertReceiveChannelCounter?.alert_groups_count
}
/>
<PluginLink
className={cx('hover-button')}
target="_blank"
query={{ page: 'alert-groups', integration: alertReceiveChannel.id }}
>
<TooltipBadge
borderType="primary"
tooltipTitle={undefined}
tooltipContent={this.getAlertReceiveChannelCounterTooltip()}
text={
alertReceiveChannelCounter?.alerts_count + '/' + alertReceiveChannelCounter?.alert_groups_count
}
/>
</PluginLink>
)}
<TooltipBadge
@ -268,11 +240,9 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
<HorizontalGroup spacing="xs">
<Text type="secondary">Type:</Text>
<HorizontalGroup spacing="none">
<HorizontalGroup spacing="xs">
<IntegrationLogo scale={0.08} integration={integration} />
<Text type="secondary" size="small">
{integration?.display_name}
</Text>
<Text type="primary">{integration?.display_name}</Text>
</HorizontalGroup>
</HorizontalGroup>
<HorizontalGroup spacing="xs">
@ -295,77 +265,87 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
expandedView: <HowToConnectComponent id={id} />,
},
{
customIcon: 'layer-group',
isExpanded: false,
isCollapsible: true,
collapsedView: (
isCollapsible: false,
canHoverIcon: false,
expandedView: (
<IntegrationBlock
hasCollapsedBorder
heading={
<HorizontalGroup spacing={'md'}>
<Tag color={getVar('--tag-secondary')} className={cx('tag')}>
<div className={cx('templates__container')}>
<Tag
color={getVar('--tag-secondary-transparent')}
border={getVar('--border-weak')}
className={cx('tag')}
>
<Text type="primary" size="small">
Templates
</Text>
</Tag>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Grouping:</Text>
<Text type="link">
{IntegrationHelper.truncateLine(templates['grouping_id_template'] || '')}
</Text>
</HorizontalGroup>
<div className={cx('templates__content')}>
<HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Grouping:</Text>
<Text type="primary">
{IntegrationHelper.truncateLine(templates['grouping_id_template'] || '')}
</Text>
</HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Autoresolve:</Text>
<Text type="link">
{IntegrationHelper.truncateLine(templates['resolve_condition_template'] || '')}
</Text>
</HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Autoresolve:</Text>
<Text type="primary">
{IntegrationHelper.truncateLine(templates['resolve_condition_template'] || '')}
</Text>
</HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Visualisation:</Text>
<Text type="primary">Multiple</Text>
</HorizontalGroup>
</HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Visualisation:</Text>
<Text type="primary">Multiple</Text>
</HorizontalGroup>
</HorizontalGroup>
<div className={cx('templates__edit')}>
<Button
variant={'secondary'}
icon="edit"
size={'sm'}
tooltip="Edit"
onClick={() => this.setState({ isTemplateSettingsOpen: true })}
/>
</div>
</div>
</div>
}
content={null}
/>
),
expandedView: (
<IntegrationBlock
hasCollapsedBorder
heading={
<HorizontalGroup>
<Tag color={getVar('--tag-secondary')} className={cx('tag')}>
<Text type="primary" size="small">
Templates
</Text>
</Tag>
</HorizontalGroup>
}
content={
<IntegrationTemplateList
getTemplatesList={this.getTemplatesList}
openEditTemplateModal={this.openEditTemplateModal}
templates={templates}
/>
}
/>
),
collapsedView: undefined,
},
{
customIcon: 'plus',
customIcon: 'code-branch',
isCollapsible: false,
collapsedView: null,
canHoverIcon: false,
expandedView: (
<div className={cx('routesSection')}>
<VerticalGroup spacing="md">
<Text type={'primary'}>Routes</Text>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Button variant={'primary'} onClick={this.handleAddNewRoute}>
Add route
</Button>
</WithPermissionControlTooltip>
<Text type={'primary'} className={cx('routesSection__heading')}>
Routes
</Text>
<HorizontalGroup>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Button
variant={'primary'}
className={cx('routesSection__add')}
onClick={this.handleAddNewRoute}
>
Add route
</Button>
</WithPermissionControlTooltip>
{this.state.isAddingRoute && <LoadingPlaceholder text="Loading..." />}
</HorizontalGroup>
</VerticalGroup>
</div>
),
@ -374,28 +354,22 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
]}
/>
<IntegrationSendDemoPayloadModal
alertReceiveChannel={alertReceiveChannel}
isOpen={isDemoModalOpen}
onHideOrCancel={() => this.setState({ isDemoModalOpen: false })}
/>
{isEditTemplateModalOpen && (
<IntegrationTemplate
id={id}
onHide={() => {
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<Integration2Props, Integration2State>
);
}
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<Integration2Props, Integration2State>
const templates = alertReceiveChannelStore.templates[id];
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[id];
return channelFilterIds.map((channelFilterId: ChannelFilter['id'], routeIndex: number) => ({
isCollapsible: true,
isExpanded: false,
collapsedView: (
<CollapsedIntegrationRouteDisplay
alertReceiveChannelId={id}
channelFilterId={channelFilterId}
routeIndex={routeIndex}
/>
),
expandedView: (
<ExpandedIntegrationRouteDisplay
alertReceiveChannelId={id}
channelFilterId={channelFilterId}
routeIndex={routeIndex}
templates={templates}
openEditTemplateModal={this.openEditTemplateModal}
onEditRegexpTemplate={this.handleEditRegexpRouteTemplate}
/>
),
}));
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: (
<CollapsedIntegrationRouteDisplay
alertReceiveChannelId={id}
channelFilterId={channelFilterId}
routeIndex={routeIndex}
/>
),
expandedView: (
<ExpandedIntegrationRouteDisplay
alertReceiveChannelId={id}
channelFilterId={channelFilterId}
routeIndex={routeIndex}
templates={templates}
openEditTemplateModal={this.openEditTemplateModal}
onEditRegexpTemplate={this.handleEditRegexpRouteTemplate}
/>
),
} as IntegrationCollapsibleItem)
);
};
renderHearbeat = (alertReceiveChannel: AlertReceiveChannel) => {
@ -473,6 +484,10 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
const heartbeatStatus = Boolean(heartbeat?.status);
if (!alertReceiveChannel.heartbeat) {
return null;
}
return (
<TooltipBadge
text={undefined}
@ -507,38 +522,9 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
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<Integration2Props, Integration2State>
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<Integration2Props, Integration2State>
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<Integration2Props, Integration2State>
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<IntegrationSendDemoPayloadModalP
onHideOrCancel,
}) => {
const { alertReceiveChannelStore } = useStore();
const [demoPayload, setDemoPayload] = useState<string>(
JSON.stringify(alertReceiveChannel.demo_alert_payload, null, '\t')
);
let onPayloadChangeDebounced = debounce(100, onPayloadChange);
return (
<Modal
closeOnBackdropClick={false}
closeOnEscape
isOpen={isOpen}
onDismiss={onHideOrCancel}
@ -698,12 +680,31 @@ const IntegrationSendDemoPayloadModal: React.FC<IntegrationSendDemoPayloadModalP
<VerticalGroup>
<HorizontalGroup spacing={'xs'}>
<Text type={'secondary'}>Alert Payload</Text>
<Tooltip content={'TODO'} placement={'top-start'}>
<Tooltip
content={
<>
A demo alert will be generated. You can find it on the <strong>Alert Groups</strong> page
</>
}
placement={'top-start'}
>
<Icon name={'info-circle'} />
</Tooltip>
</HorizontalGroup>
<SourceCode showCopyToClipboard={false}>{getDemoAlertJSON()}</SourceCode>
<div className={cx('integration__payloadInput')}>
<MonacoEditor
value={JSON.stringify(alertReceiveChannel.demo_alert_payload, null, '\t')}
disabled={true}
height={`200px`}
useAutoCompleteList={false}
language={MONACO_LANGUAGE.json}
data={undefined}
monacoOptions={MONACO_PAYLOAD_OPTIONS}
showLineNumbers={false}
onChange={onPayloadChangeDebounced}
/>
</div>
<HorizontalGroup justify={'flex-end'} spacing={'md'}>
<Button variant={'secondary'} onClick={onHideOrCancel}>
@ -720,8 +721,17 @@ const IntegrationSendDemoPayloadModal: React.FC<IntegrationSendDemoPayloadModalP
</Modal>
);
function onPayloadChange(value: string) {
setDemoPayload(value);
}
function sendDemoAlert() {
alertReceiveChannelStore.sendDemoAlert(alertReceiveChannel.id).then(() => {
let parsedPayload = undefined;
try {
parsedPayload = JSON.parse(demoPayload);
} catch (ex) {}
alertReceiveChannelStore.sendDemoAlert(alertReceiveChannel.id, parsedPayload).then(() => {
alertReceiveChannelStore.updateCounters();
openNotification(<DemoNotification />);
onHideOrCancel();
@ -729,14 +739,210 @@ const IntegrationSendDemoPayloadModal: React.FC<IntegrationSendDemoPayloadModalP
}
function getCurlText() {
// TODO add this
return `curl -X POST [URL]
-H "Content-Type: application/json"
-d "[JSON data]"`;
return (
`curl '${API_HOST}${API_PATH_PREFIX}${API_PATH_PREFIX}/alert_receive_channels/${alertReceiveChannel.id}/send_demo_alert/'` +
` -XPOST -H 'Content-Type: application/json'` +
`--data-raw '{"demo_alert_payload":{"alerts":[{"a":"b"}]}}' --compressed`
);
}
};
interface IntegrationActionsProps {
alertReceiveChannel: AlertReceiveChannel;
}
const IntegrationActions: React.FC<IntegrationActionsProps> = ({ alertReceiveChannel }) => {
const { maintenanceStore, alertReceiveChannelStore } = useStore();
const history = useHistory();
const [confirmModal, setConfirmModal] = useState<{
isOpen: boolean;
title: any;
dismissText: string;
confirmText: string;
body?: React.ReactNode;
description?: string;
confirmationText?: string;
onConfirm: () => void;
}>(undefined);
const [isIntegrationSettingsOpen, setIsIntegrationSettingsOpen] = useState(false);
const [isHearbeatFormOpen, setIsHearbeatFormOpen] = useState(false);
const [isDemoModalOpen, setIsDemoModalOpen] = useState(false);
const [maintenanceData, setMaintenanceData] = useState<{
disabled: boolean;
alert_receive_channel_id: AlertReceiveChannel['id'];
}>(undefined);
const { id } = alertReceiveChannel;
return (
<>
{confirmModal && (
<ConfirmModal
isOpen={confirmModal.isOpen}
title={confirmModal.title}
confirmText={confirmModal.confirmText}
dismissText="Cancel"
body={confirmModal.body}
description={confirmModal.description}
confirmationText={confirmModal.confirmationText}
onConfirm={confirmModal.onConfirm}
onDismiss={() => setConfirmModal(undefined)}
/>
)}
{alertReceiveChannel.demo_alert_enabled && (
<IntegrationSendDemoPayloadModal
alertReceiveChannel={alertReceiveChannel}
isOpen={isDemoModalOpen}
onHideOrCancel={() => setIsDemoModalOpen(false)}
/>
)}
{isIntegrationSettingsOpen && (
<IntegrationForm2
isTableView={false}
onHide={() => setIsIntegrationSettingsOpen(false)}
onUpdate={() => alertReceiveChannelStore.updateItem(alertReceiveChannel['id'])}
id={alertReceiveChannel['id']}
/>
)}
{isHearbeatFormOpen && (
<Integration2HeartbeatForm
alertReceveChannelId={alertReceiveChannel['id']}
onClose={() => setIsHearbeatFormOpen(false)}
/>
)}
{maintenanceData && (
<MaintenanceForm
initialData={maintenanceData}
onUpdate={() => alertReceiveChannelStore.updateItem(alertReceiveChannel.id)}
onHide={() => setMaintenanceData(undefined)}
/>
)}
<div className={cx('integration__actions')}>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsTest}>
<Button
variant="secondary"
size="md"
onClick={() => setIsDemoModalOpen(true)}
data-testid="send-demo-alert"
disabled={!alertReceiveChannel.demo_alert_enabled}
tooltip={alertReceiveChannel.demo_alert_enabled ? '' : 'Demo Alerts are not enabled for this integration'}
>
Send demo alert
</Button>
</WithPermissionControlTooltip>
<WithContextMenu
renderMenuItems={() => (
<div className={cx('integration__actionsList')} id="integration-menu-options">
<div className={cx('integration__actionItem')} onClick={() => openIntegrationSettings()}>
<Text type="primary">Integration Settings</Text>
</div>
<div className={cx('integration__actionItem')} onClick={() => setIsHearbeatFormOpen(true)}>
Hearbeat
</div>
{!alertReceiveChannel.maintenance_till && (
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
<div className={cx('integration__actionItem')} onClick={openStartMaintenance}>
<Text type="primary">Start Maintenance</Text>
</div>
</WithPermissionControlTooltip>
)}
{alertReceiveChannel.maintenance_till && (
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
<div className={cx('integration__actionItem')}>
<div
onClick={() => {
setConfirmModal({
isOpen: true,
confirmText: 'Stop',
dismissText: 'Cancel',
onConfirm: onStopMaintenance,
title: (
<>
Are you sure you want to stop the maintenance for{' '}
<Emoji text={alertReceiveChannel.verbal_name} />?
</>
),
});
}}
>
<Text type="primary">Stop Maintenance</Text>
</div>
</div>
</WithPermissionControlTooltip>
)}
<div className="thin-line-break" />
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<div className={cx('integration__actionItem')}>
<div
onClick={() => {
setConfirmModal({
isOpen: true,
title: (
<>
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} />{' '}
integration?
</>
),
body: <>This action cannot be undone.</>,
onConfirm: deleteIntegration,
dismissText: 'Cancel',
confirmText: 'Delete',
});
}}
>
<Text type="danger">
<HorizontalGroup spacing={'xs'}>
<Icon name="trash-alt" />
<span>Delete Integration</span>
</HorizontalGroup>
</Text>
</div>
</div>
</WithPermissionControlTooltip>
</div>
)}
>
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} />}
</WithContextMenu>
</div>
</>
);
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={
<div className={cx('how-to-connect__container')}>
<Tag color={getVar('--tag-secondary')} className={cx('how-to-connect__tag')}>
<Tag
color={getVar('--tag-secondary-transparent')}
border={getVar('--border-weak')}
className={cx('how-to-connect__tag')}
>
<Text type="primary" size="small">
HTTP Endpoint
</Text>
</Tag>
<IntegrationMaskedInputField value={alertReceiveChannelStore.items[id].integration_url} />
<IntegrationInputField
value={alertReceiveChannelStore.items[id].integration_url}
className={cx('integration__input-field')}
/>
<a href="https://grafana.com/docs/oncall/latest/integrations/" target="_blank" rel="noreferrer">
<Text type="link" size="small" onClick={openHowToConnect}>
<Text type="link" size="small">
<HorizontalGroup>
How to connect
<Icon name="external-link-alt" />
@ -772,15 +985,13 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
/>
);
function openHowToConnect() {}
function renderContent() {
return (
<div className={cx('integration__alertsPanel')}>
<VerticalGroup justify={'flex-start'} spacing={'xs'}>
{!hasAlerts && (
<HorizontalGroup spacing={'xs'}>
<LoadingPlaceholder text="" className={cx('loadingPlaceholder')} />
<Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />
<Text type={'primary'}>No alerts yet; try to send a demo alert</Text>
</HorizontalGroup>
)}

View file

@ -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<number>(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 (
<Drawer width={'640px'} scrollableContent title={'Heartbeat'} onClose={onClose} closeOnMaskClick={false}>
<VerticalGroup spacing={'lg'}>
<Text type="secondary">
Start maintenance mode when performing scheduled maintenance or updates on the infrastructure, which may
trigger false alarms.
</Text>
<VerticalGroup spacing="md">
<div className={cx('u-width-100')}>
<Field label={'Setup heartbeat interval'}>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Select
className={cx('select', 'timeout')}
onChange={(value: SelectableValue) => setInterval(value.value)}
placeholder="Heartbeat Timeout"
value={interval}
options={(timeoutOptions || []).map((timeoutOption: SelectOption) => ({
value: timeoutOption.value,
label: timeoutOption.display_name,
}))}
/>
</WithPermissionControlTooltip>
</Field>
</div>
<div className={cx('u-width-100')}>
<Field label="Endpoint" description="Use the following unique Grafana link to send GET and POST requests">
<IntegrationInputField value={alertReceiveChannel?.integration_url} showEye={false} isMasked={false} />
</Field>
</div>
</VerticalGroup>
<VerticalGroup style={{ marginTop: 'auto' }}>
<HorizontalGroup className={cx('buttons')} justify="flex-end">
<Button variant={'secondary'} onClick={onClose}>
Cancel
</Button>
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
<Button variant="primary" onClick={onSave}>
{alertReceiveChannel.heartbeat ? 'Save' : 'Create'}
</Button>
</WithPermissionControlTooltip>
</HorizontalGroup>
</VerticalGroup>
</VerticalGroup>
</Drawer>
);
async function onSave() {
const heartbeat = alertReceiveChannel.heartbeat;
if (heartbeat) {
await heartbeatStore.saveHeartbeat(heartbeat.id, {
alert_receive_channel: heartbeat.alert_receive_channel,
timeout_seconds: interval,
});
onClose();
} else {
await heartbeatStore.createHeartbeat(alertReceveChannelId, {
timeout_seconds: interval,
});
onClose();
}
await alertReceiveChannelStore.updateItem(alertReceveChannelId);
}
});
export default withMobXProviderContext(Integration2HearbeatForm) as ({
alertReceveChannelId,
}: Integration2HearbeatFormProps) => JSX.Element;

View file

@ -11,10 +11,10 @@
.integrationBlock__content {
background: var(--background-primary);
border: var(--border-weak);
padding-bottom: 0;
&--collapsedBorder {
border-left: none;
padding-left: 0;
padding-bottom: 0;
}
}

View file

@ -9,17 +9,20 @@ import styles from './IntegrationBlock.module.scss';
const cx = cn.bind(styles);
interface IntegrationBlockProps {
className?: string;
hasCollapsedBorder: boolean;
heading: React.ReactNode;
content: React.ReactNode;
}
const IntegrationBlock: React.FC<IntegrationBlockProps> = ({ heading, content, hasCollapsedBorder }) => {
const IntegrationBlock: React.FC<IntegrationBlockProps> = ({ heading, content, hasCollapsedBorder, className }) => {
return (
<div className={cx('integrationBlock')}>
<Block bordered shadowed className={cx('integrationBlock__heading')}>
{heading}
</Block>
<div className={cx('integrationBlock', className)}>
{heading && (
<Block bordered shadowed className={cx('integrationBlock__heading')}>
{heading}
</Block>
)}
{content && (
<div
className={cx('integrationBlock__content', {

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Button, HorizontalGroup, Icon, InlineLabel } from '@grafana/ui';
import { Button, HorizontalGroup, Icon, InlineLabel, LoadingPlaceholder, Tooltip } from '@grafana/ui';
import Text from 'components/Text/Text';
@ -8,8 +8,8 @@ interface IntegrationTemplateBlockProps {
label: string;
labelTooltip?: string;
renderInput: () => React.ReactNode;
showClose?: boolean;
showHelp?: boolean;
isLoading?: boolean;
onEdit: (templateName) => void;
onRemove?: () => void;
@ -20,11 +20,11 @@ const IntegrationTemplateBlock: React.FC<IntegrationTemplateBlockProps> = ({
label,
labelTooltip,
renderInput,
showClose,
showHelp,
onEdit,
onHelp,
onRemove,
isLoading,
}) => {
let inlineLabelProps = { labelTooltip };
if (!labelTooltip) {
@ -32,19 +32,26 @@ const IntegrationTemplateBlock: React.FC<IntegrationTemplateBlockProps> = ({
}
return (
<HorizontalGroup align={'flex-start'}>
<HorizontalGroup align={'flex-start'} spacing={'xs'}>
<InlineLabel width={20} {...inlineLabelProps}>
{label}
</InlineLabel>
{renderInput()}
<Button variant={'secondary'} icon={'edit'} size={'md'} onClick={onEdit} />
{showClose && <Button variant={'secondary'} icon={'times'} size={'md'} onClick={onRemove} />}
<Tooltip content={'Edit'}>
<Button variant={'secondary'} icon={'edit'} tooltip="Edit" size={'md'} onClick={onEdit} />
</Tooltip>
<Tooltip content={'Reset Template to default'}>
<Button variant={'secondary'} icon={'times'} size={'md'} onClick={onRemove} />
</Tooltip>
{showHelp && (
<Button variant="secondary" size="md" onClick={onHelp}>
<Text type="link">Help</Text>
<Icon name="angle-down" size="sm" />
</Button>
)}
{isLoading && <LoadingPlaceholder text="Loading..." />}
</HorizontalGroup>
);
};

View file

@ -1,11 +1,14 @@
import React from 'react';
import React, { useState } from 'react';
import { ButtonCascader, CascaderOption, VerticalGroup } from '@grafana/ui';
import { ConfirmModal, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor';
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
import Text from 'components/Text/Text';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { AlertTemplatesDTO } from 'models/alert_templates';
import { useStore } from 'state/useStore';
import { openErrorNotification, openNotification } from 'utils';
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_INPUT_HEIGHT_TALL, MONACO_OPTIONS } from './Integration2.config';
import IntegrationHelper from './Integration2.helper';
@ -17,31 +20,36 @@ const cx = cn.bind(styles);
interface IntegrationTemplateListProps {
templates: AlertTemplatesDTO[];
getTemplatesList(): CascaderOption[];
alertReceiveChannelId: AlertReceiveChannel['id'];
openEditTemplateModal: (templateName: string | string[]) => void;
}
const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
templates,
openEditTemplateModal,
getTemplatesList,
alertReceiveChannelId,
}) => {
const isAutoAcknOrSourceLinkChanged =
!templates['acknowledge_condition_template_is_default'] || !templates['source_link_template'];
const isPhoneOrSMSChanged =
!templates['sms_title_template_is_default'] || !templates['phone_call_title_template_is_default'];
const isSlackChanged =
!templates['slack_title_template_is_default'] ||
!templates['slack_message_template_is_default'] ||
!templates['slack_image_url_template_is_default'];
const isTelegramChanged =
!templates['telegram_title_template_is_default'] ||
!templates['telegram_message_template_is_default'] ||
!templates['telegram_image_url_template_is_default'];
const isEmailOrMessageChanged = !templates['email_title_template_is_default'] || !templates['email_message_template'];
const { alertReceiveChannelStore } = useStore();
const [isRestoringTemplate, setIsRestoringTemplate] = useState<boolean>(false);
const [templateRestoreName, setTemplateRestoreName] = useState<string>(undefined);
const [showConfirmModal, setShowConfirmModal] = useState(false);
return (
<div className={cx('integration__templates')}>
{showConfirmModal && (
<ConfirmModal
isOpen={true}
title={undefined}
confirmText={'Reset'}
dismissText="Cancel"
body={'Are you sure you want to reset Slack Title template to default state?'}
description={undefined}
confirmationText={undefined}
onConfirm={() => onResetTemplate(templateRestoreName)}
onDismiss={() => onDismiss()}
/>
)}
<IntegrationBlockItem>
<Text type="secondary">
Templates are used to interpret alert from monitoring. Reduce noise, customize visualization
@ -51,10 +59,12 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
<IntegrationBlockItem>
<VerticalGroup>
<IntegrationTemplateBlock
onRemove={() => onShowConfirmModal('grouping_id_template')}
isLoading={isRestoringTemplate && templateRestoreName === 'grouping_id_template'}
label={'Grouping'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoJinja2Editor
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['grouping_id_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
@ -64,15 +74,16 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
/>
</div>
)}
showHelp
onEdit={() => openEditTemplateModal('grouping_id_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'resolve_condition_template'}
onRemove={() => onShowConfirmModal('resolve_condition_template')}
label={'Auto resolve'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoJinja2Editor
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['resolve_condition_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
@ -92,10 +103,12 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
<Text type={'primary'}>Web</Text>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'web_title_template'}
onRemove={() => onShowConfirmModal('web_title_template')}
label={'Title'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['web_title_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
@ -109,10 +122,12 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'web_message_template'}
onRemove={() => onShowConfirmModal('web_message_template')}
label={'Message'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['web_message_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
@ -126,10 +141,12 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'web_image_url_template'}
onRemove={() => onShowConfirmModal('web_image_url_template')}
label={'Image'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['web_image_url_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
@ -144,305 +161,294 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
</VerticalGroup>
</IntegrationBlockItem>
{isAutoAcknOrSourceLinkChanged && (
<IntegrationBlockItem>
<VerticalGroup>
{!templates['acknowledge_condition_template_is_default'] && (
<IntegrationTemplateBlock
label={'Auto acknowledge'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(
templates['acknowledge_condition_template'] || '',
false
)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('acknowledge_condition_template')}
showHelp
/>
<IntegrationBlockItem>
<VerticalGroup>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'acknowledge_condition_template'}
onRemove={() => onShowConfirmModal('acknowledge_condition_template')}
label={'Auto acknowledge'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(
templates['acknowledge_condition_template'] || '',
false
)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('acknowledge_condition_template')}
/>
{!templates['source_link_template_is_default'] && (
<IntegrationTemplateBlock
label={'Source Link'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['source_link_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('source_link_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'source_link_template'}
onRemove={() => onShowConfirmModal('source_link_template')}
label={'Source Link'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['source_link_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
</VerticalGroup>
</IntegrationBlockItem>
)}
{isPhoneOrSMSChanged && (
<IntegrationBlockItem>
<VerticalGroup>
{!templates['phone_call_title_template_is_default'] && (
<IntegrationTemplateBlock
label={'Phone Call'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['phone_call_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('phone_call_title_template')}
showHelp
/>
)}
{!templates['sms_title_template_is_default'] && (
<IntegrationTemplateBlock
label={'SMS'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['sms_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('sms_title_template')}
/>
)}
</VerticalGroup>
</IntegrationBlockItem>
)}
{isSlackChanged && (
<IntegrationBlockItem>
<VerticalGroup>
<Text type={'primary'}>Slack</Text>
{!templates['slack_title_template_is_default'] && (
<IntegrationTemplateBlock
label={'Title'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['slack_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('slack_title_template')}
/>
)}
{!templates['slack_message_template_is_default'] && (
<IntegrationTemplateBlock
label={'Message'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['slack_message_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('slack_message_template')}
/>
)}
{!templates['slack_image_url_template_is_default'] && (
<IntegrationTemplateBlock
label={'Image'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['slack_image_url_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('slack_image_url_template')}
/>
)}
</VerticalGroup>
</IntegrationBlockItem>
)}
{isTelegramChanged && (
<IntegrationBlockItem>
<VerticalGroup>
<Text type={'primary'}>Telegram</Text>
{!templates['telegram_title_template_is_default'] && (
<IntegrationTemplateBlock
label={'Title'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['telegram_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('telegram_title_template')}
/>
)}
{!templates['telegram_message_template_is_default'] && (
<IntegrationTemplateBlock
label={'Message'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['telegram_message_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('telegram_message_template')}
/>
)}
{!templates['telegram_image_url_template_is_default'] && (
<IntegrationTemplateBlock
label={'Image'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(
templates['telegram_image_url_template'] || '',
false
)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('telegram_image_url_template')}
/>
)}
</VerticalGroup>
</IntegrationBlockItem>
)}
{isEmailOrMessageChanged && (
<IntegrationBlockItem>
<VerticalGroup>
<Text type={'primary'}>Email</Text>
{!templates['email_title_template_is_default'] && (
<IntegrationTemplateBlock
label={'Title'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['email_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('email_title_template')}
/>
)}
{!templates['email_message_template_is_default'] && (
<IntegrationTemplateBlock
label={'Message'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoJinja2Editor
value={IntegrationHelper.getFilteredTemplate(templates['email_message_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('email_message_template')}
/>
)}
</VerticalGroup>
</IntegrationBlockItem>
)}
onEdit={() => openEditTemplateModal('source_link_template')}
/>
</VerticalGroup>
</IntegrationBlockItem>
<IntegrationBlockItem>
<VerticalGroup>
<Text type={'secondary'}>By default alert groups rendered based on Web templates.</Text>
<Text type={'secondary'}>
Customise how they rendered in SMS, Phone Calls, Mobile App, Slack, Telegram, MS Teams{' '}
</Text>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'phone_call_title_template'}
onRemove={() => onShowConfirmModal('phone_call_title_template')}
label={'Phone Call'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['phone_call_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('phone_call_title_template')}
/>
<div className={cx('customise-button')}>
<ButtonCascader
variant="secondary"
onChange={(_key) => {
if (Object.values(_key).length > 1) {
openEditTemplateModal(Object.values(_key)[1]);
} else {
openEditTemplateModal(_key);
}
}}
options={getTemplatesList()}
icon="plus"
value={undefined}
buttonProps={{ size: 'sm' }}
>
Customise templates
</ButtonCascader>
</div>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'sms_title_template'}
onRemove={() => onShowConfirmModal('sms_title_template')}
label={'SMS'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['sms_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('sms_title_template')}
/>
</VerticalGroup>
</IntegrationBlockItem>
<IntegrationBlockItem>
<VerticalGroup>
<Text type={'primary'}>Slack</Text>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'slack_title_template'}
onRemove={() => onShowConfirmModal('slack_title_template')}
label={'Title'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['slack_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('slack_title_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'slack_message_template'}
onRemove={() => onShowConfirmModal('slack_message_template')}
label={'Message'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['slack_message_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('slack_message_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'slack_image_url_template'}
onRemove={() => onShowConfirmModal('slack_image_url_template')}
label={'Image'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['slack_image_url_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('slack_image_url_template')}
/>
</VerticalGroup>
</IntegrationBlockItem>
<IntegrationBlockItem>
<VerticalGroup>
<Text type={'primary'}>Telegram</Text>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'telegram_title_template'}
onRemove={() => onShowConfirmModal('telegram_title_template')}
label={'Title'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['telegram_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('telegram_title_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'telegram_message_template'}
onRemove={() => onShowConfirmModal('telegram_message_template')}
label={'Message'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['telegram_message_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('telegram_message_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'telegram_image_url_template'}
onRemove={() => onShowConfirmModal('telegram_image_url_template')}
label={'Image'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['telegram_image_url_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('telegram_image_url_template')}
/>
</VerticalGroup>
</IntegrationBlockItem>
<IntegrationBlockItem>
<VerticalGroup>
<Text type={'primary'}>Email</Text>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'email_title_template'}
onRemove={() => onShowConfirmModal('email_title_template')}
label={'Title'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['email_title_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('email_title_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'email_message_template'}
onRemove={() => onShowConfirmModal('email_message_template')}
label={'Message'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['email_message_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('email_message_template')}
/>
</VerticalGroup>
</IntegrationBlockItem>
</div>
);
function onShowConfirmModal(templateName: string) {
setTemplateRestoreName(templateName);
setShowConfirmModal(true);
}
function onDismiss() {
setTemplateRestoreName(undefined);
setShowConfirmModal(false);
}
function onResetTemplate(templateName: string) {
setTemplateRestoreName(undefined);
setIsRestoringTemplate(true);
alertReceiveChannelStore
.saveTemplates(alertReceiveChannelId, { [templateName]: '' })
.then(() => {
openNotification('The Alert template has been updated');
})
.catch((err) => {
if (err.response?.data?.length > 0) {
openErrorNotification(err.response.data);
} else {
openErrorNotification(err.message);
}
})
.finally(() => {
setIsRestoringTemplate(false);
setShowConfirmModal(false);
});
}
};
export default IntegrationTemplateList;

View file

@ -24,7 +24,7 @@ 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 { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
import IntegrationHelper from 'pages/integration_2/Integration2.helper';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';

View file

@ -29,6 +29,7 @@ interface MaintenancePageState {
maintenanceData?: {
type?: MaintenanceType;
alert_receive_channel_id?: AlertReceiveChannel['id'];
disabled?: boolean;
};
}

View file

@ -21,6 +21,7 @@
--tag-warning: #c69b06;
--tag-primary: #299c46;
--tag-secondary: #464c54;
--tag-secondary-transparent: rgba(204, 204, 220, 0.07);
--tag-background-primary: rgba(56, 113, 220, 0.2);
--tag-border-primary: rgba(56, 113, 220, 0.2);
--tag-background-danger: rgba(242, 73, 92, 0.15);