commit
32d7cb032f
55 changed files with 1597 additions and 1016 deletions
|
|
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v1.2.24 (2023-05-17)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed bug in Escalation Chains where reordering an item crashed the list
|
||||
|
||||
## v1.2.23 (2023-05-15)
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class AlertReceiveChannelSerializer(EagerLoadingMixin, serializers.ModelSerializ
|
|||
maintenance_till = serializers.ReadOnlyField(source="till_maintenance_timestamp")
|
||||
heartbeat = serializers.SerializerMethodField()
|
||||
allow_delete = serializers.SerializerMethodField()
|
||||
description_short = serializers.CharField(max_length=250, required=False)
|
||||
description_short = serializers.CharField(max_length=250, required=False, allow_null=True)
|
||||
demo_alert_payload = serializers.SerializerMethodField()
|
||||
routes_count = serializers.SerializerMethodField()
|
||||
connected_escalations_chains_count = serializers.SerializerMethodField()
|
||||
|
|
|
|||
|
|
@ -247,8 +247,9 @@ class GcomAPIClient(APIClient):
|
|||
return self.api_get(query)
|
||||
|
||||
def is_stack_deleted(self, stack_id: str) -> bool:
|
||||
instance_info = self.get_instance_info(stack_id)
|
||||
return instance_info and instance_info.get("status") == self.STACK_STATUS_DELETED
|
||||
url = f"instances?includeDeleted=true&id={stack_id}"
|
||||
instance_infos, _ = self.api_get(url)
|
||||
return instance_infos["items"] and instance_infos["items"][0].get("status") == self.STACK_STATUS_DELETED
|
||||
|
||||
def post_active_users(self, body):
|
||||
return self.api_post("app-active-users", body)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class IntegrationSerializer(EagerLoadingMixin, serializers.ModelSerializer, Main
|
|||
templates = serializers.DictField(required=False)
|
||||
default_route = serializers.DictField(required=False)
|
||||
heartbeat = serializers.SerializerMethodField()
|
||||
description_short = serializers.CharField(max_length=250, required=False)
|
||||
description_short = serializers.CharField(max_length=250, required=False, allow_null=True)
|
||||
|
||||
PREFETCH_RELATED = ["channel_filters"]
|
||||
SELECT_RELATED = ["organization", "integration_heartbeat"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
|
@ -126,12 +127,14 @@ def check_grafana_incident_is_enabled(client):
|
|||
def delete_organization_if_needed(organization):
|
||||
# Organization has a manually set API token, it will not be found within GCOM
|
||||
# and would need to be deleted manually.
|
||||
if organization.gcom_token is None:
|
||||
logger.info(f"Organization {organization.pk} has no gcom_token. Probably it's needed to delete org manually.")
|
||||
PluginAuthToken = apps.get_model("auth_token", "PluginAuthToken")
|
||||
manually_provisioned_token = PluginAuthToken.objects.filter(organization_id=organization.pk).first()
|
||||
if manually_provisioned_token:
|
||||
logger.info(f"Organization {organization.pk} has PluginAuthToken. Probably it's needed to delete org manually.")
|
||||
return False
|
||||
|
||||
# Use common token as organization.gcom_token could be already revoked
|
||||
client = GcomAPIClient(settings.GRAFANA_COM_API_TOKEN)
|
||||
client = GcomAPIClient(settings.GRAFANA_COM_ADMIN_API_TOKEN)
|
||||
is_stack_deleted = client.is_stack_deleted(organization.stack_id)
|
||||
if not is_stack_deleted:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ def test_duplicate_user_ids(make_organization, make_user_for_organization):
|
|||
def test_cleanup_organization_deleted(make_organization):
|
||||
organization = make_organization(gcom_token="TEST_GCOM_TOKEN")
|
||||
|
||||
with patch.object(GcomAPIClient, "get_instance_info", return_value={"status": "deleted"}):
|
||||
with patch.object(GcomAPIClient, "api_get", return_value=({"items": [{"status": "deleted"}]}, None)):
|
||||
cleanup_organization(organization.id)
|
||||
|
||||
organization.refresh_from_db()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Main
|
||||
enabled = True
|
||||
title = "AlertManager"
|
||||
title = "Alertmanager"
|
||||
slug = "alertmanager"
|
||||
short_description = "Prometheus"
|
||||
is_displayed_on_web = True
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Main
|
||||
enabled = True
|
||||
title = "Formatted Webhook"
|
||||
title = "Formatted webhook"
|
||||
slug = "formatted_webhook"
|
||||
short_description = None
|
||||
description = None
|
||||
|
|
|
|||
|
|
@ -52,6 +52,6 @@ django-dbconn-retry==0.1.7
|
|||
django-ipware==4.0.2
|
||||
django-anymail==8.6
|
||||
django-deprecate-fields==0.1.1
|
||||
pymdown-extensions==9.11
|
||||
pymdown-extensions==10.0
|
||||
requests==2.29.0
|
||||
urllib3==1.26.15
|
||||
|
|
|
|||
|
|
@ -487,7 +487,8 @@ INTERNAL_IPS = ["127.0.0.1"]
|
|||
|
||||
SELF_IP = os.environ.get("SELF_IP")
|
||||
|
||||
SILK_PROFILER_ENABLED = getenv_boolean("SILK_PROFILER_ENABLED", default=False)
|
||||
SILK_PROFILER_ENABLED = getenv_boolean("SILK_PROFILER_ENABLED", default=False) and not IS_IN_MAINTENANCE_MODE
|
||||
|
||||
if SILK_PROFILER_ENABLED:
|
||||
SILK_PATH = os.environ.get("SILK_PATH", "silk/")
|
||||
SILKY_INTERCEPT_PERCENT = getenv_integer("SILKY_INTERCEPT_PERCENT", 100)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-right: 24px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -36,7 +36,11 @@ import styles from './EscalationPolicy.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export interface EscalationPolicyProps {
|
||||
interface ElementSortableProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface EscalationPolicyProps extends ElementSortableProps {
|
||||
data: EscalationPolicyType;
|
||||
waitDelays?: any[];
|
||||
isDisabled?: boolean;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -29,7 +29,9 @@ interface EditRegexpRouteTemplateModalProps {
|
|||
const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemplateModalProps) => {
|
||||
const { onHide, onUpdateRoute, channelFilterId, onOpenEditIntegrationTemplate, alertReceiveChannelId } = props;
|
||||
const store = useStore();
|
||||
|
||||
const regexpBody = store.alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term;
|
||||
|
||||
const [regexpTemplateBody, setRegexpTemplateBody] = useState<string>(regexpBody);
|
||||
|
||||
const templateJinja2Body = store.alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term_as_jinja2;
|
||||
|
|
@ -86,7 +88,7 @@ const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemplateMod
|
|||
</HorizontalGroup>
|
||||
|
||||
<div className={cx('regexp-template-code')}>
|
||||
<MonacoJinja2Editor
|
||||
<MonacoEditor
|
||||
value={regexpTemplateBody}
|
||||
height={'200px'}
|
||||
data={undefined}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
|
|||
|
||||
return (
|
||||
<EscalationPolicy
|
||||
index={index} // This in here is a MUST for the SortableElement
|
||||
key={`item-${escalationPolicy.id}`}
|
||||
data={escalationPolicy}
|
||||
number={index + offset + 1}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React, { useState, useCallback, ChangeEvent } from 'react';
|
|||
import { Drawer, VerticalGroup, HorizontalGroup, Input, Tag, EmptySearchResult, Button } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import Collapse from 'components/Collapse/Collapse';
|
||||
import Block from 'components/GBlock/Block';
|
||||
|
|
@ -10,10 +11,14 @@ 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 { openErrorNotification } from 'utils';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import { form } from './IntegrationForm2.config';
|
||||
import { prepareForEdit } from './IntegrationForm2.helpers';
|
||||
|
|
@ -24,14 +29,16 @@ 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();
|
||||
const history = useHistory();
|
||||
|
||||
const { alertReceiveChannelStore, userStore } = store;
|
||||
|
||||
|
|
@ -40,6 +47,7 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
const [filterValue, setFilterValue] = useState('');
|
||||
const [showNewIntegrationForm, setShowNewIntegrationForm] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<AlertReceiveChannelOption>(undefined);
|
||||
const [showIntegrarionsListDrawer, setShowIntegrarionsListDrawer] = useState(id === 'new');
|
||||
|
||||
const data =
|
||||
id === 'new'
|
||||
|
|
@ -48,7 +56,17 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
|
||||
const handleSubmit = useCallback(
|
||||
(data: Partial<AlertReceiveChannel>) => {
|
||||
(id === 'new' ? alertReceiveChannelStore.create(data) : alertReceiveChannelStore.update(id, data)).then(() => {
|
||||
(id === 'new'
|
||||
? alertReceiveChannelStore
|
||||
.create(data)
|
||||
.then((response) => {
|
||||
history.push(`${PLUGIN_ROOT}/integrations_2/${response.id}`);
|
||||
})
|
||||
.catch(() => {
|
||||
openErrorNotification('Something went wrong, please try again later.');
|
||||
})
|
||||
: alertReceiveChannelStore.update(id, data)
|
||||
).then(() => {
|
||||
onHide();
|
||||
onUpdate();
|
||||
});
|
||||
|
|
@ -60,6 +78,7 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
return () => {
|
||||
setSelectedOption(option);
|
||||
setShowNewIntegrationForm(true);
|
||||
setShowIntegrarionsListDrawer(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -77,7 +96,7 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{id === 'new' && (
|
||||
{showIntegrarionsListDrawer && (
|
||||
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
|
|
@ -96,7 +115,7 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
<Block
|
||||
bordered
|
||||
hover
|
||||
withBackground
|
||||
shadowed
|
||||
onClick={handleNewIntegrationOptionSelectCallback(alertReceiveChannelChoice)}
|
||||
key={alertReceiveChannelChoice.value}
|
||||
className={cx('card', { card_featured: alertReceiveChannelChoice.featured })}
|
||||
|
|
@ -128,52 +147,54 @@ const IntegrationForm2 = observer((props: IntegrationFormProps) => {
|
|||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
{(showNewIntegrationForm || id !== 'new') && (
|
||||
<Drawer
|
||||
scrollableContent
|
||||
title={id === 'new' ? `New ${selectedOption?.display_name} integration` : `Edit integration`}
|
||||
onClose={onHide}
|
||||
closeOnMaskClick={false}
|
||||
width="640px"
|
||||
>
|
||||
{(showNewIntegrationForm || !showIntegrarionsListDrawer) && (
|
||||
<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 you’ll 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 you’ll 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)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowNewIntegrationForm(false);
|
||||
setShowIntegrarionsListDrawer(true);
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
) : (
|
||||
|
|
@ -194,6 +215,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;
|
||||
|
|
|
|||
|
|
@ -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,11 +13,12 @@ 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';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
|
@ -31,13 +32,14 @@ interface IntegrationTemplateProps {
|
|||
channelFilterId?: ChannelFilter['id'];
|
||||
template: TemplateForEdit;
|
||||
templateBody: string;
|
||||
templates: AlertTemplatesDTO[];
|
||||
onHide: () => void;
|
||||
onUpdateTemplates: (values: any) => void;
|
||||
onUpdateRoute: (values: any, channelFilterId?: ChannelFilter['id']) => void;
|
||||
}
|
||||
|
||||
const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
||||
const { id, onHide, template, onUpdateTemplates, onUpdateRoute, templateBody, channelFilterId } = props;
|
||||
const { id, onHide, template, onUpdateTemplates, onUpdateRoute, templateBody, channelFilterId, templates } = props;
|
||||
|
||||
const [isCheatSheetVisible, setIsCheatSheetVisible] = useState<boolean>(false);
|
||||
const [chatOps, setChatOps] = useState(undefined);
|
||||
|
|
@ -45,14 +47,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 +66,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
const getChangeHandler = () => {
|
||||
return debounce((value: string) => {
|
||||
setChangedTemplateBody(value);
|
||||
}, 1000);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const onEditPayload = (alertPayload: string) => {
|
||||
|
|
@ -172,6 +171,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
alertReceiveChannelId={id}
|
||||
onEditPayload={onEditPayload}
|
||||
onSelectAlertGroup={onSelectAlertGroup}
|
||||
templates={templates}
|
||||
/>
|
||||
{isCheatSheetVisible ? (
|
||||
<CheatSheet cheatSheetData={getCheatSheet(template.displayName)} onClose={onCloseCheatSheet} />
|
||||
|
|
@ -188,9 +188,9 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
|
||||
<MonacoJinja2Editor
|
||||
<MonacoEditor
|
||||
value={templateBody}
|
||||
data={undefined}
|
||||
data={templates}
|
||||
showLineNumbers={true}
|
||||
height={'85vh'}
|
||||
onChange={getChangeHandler()}
|
||||
|
|
@ -198,7 +198,6 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* {alertGroupPayload || resultError ? ( */}
|
||||
<Result
|
||||
alertReceiveChannelId={id}
|
||||
templateName={template.name}
|
||||
|
|
@ -209,13 +208,6 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
error={resultError}
|
||||
onSaveAndFollowLink={onSaveAndFollowLink}
|
||||
/>
|
||||
{/* ) : (
|
||||
<div className={cx('template-block-result')}>
|
||||
<div className={cx('template-block-title')}>
|
||||
<Text>Please select Alert group to see end result</Text>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,24 @@
|
|||
border-right: none;
|
||||
}
|
||||
|
||||
.alert-groups-list {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.alert-groups-list button {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.alert-groups-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-alert-groups-badge {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-alert-groups-badge > div {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Tooltip, Icon, VerticalGroup, IconButton, Badge } from '@grafana/ui';
|
||||
import {
|
||||
Button,
|
||||
HorizontalGroup,
|
||||
Tooltip,
|
||||
Icon,
|
||||
VerticalGroup,
|
||||
IconButton,
|
||||
Badge,
|
||||
LoadingPlaceholder,
|
||||
} from '@grafana/ui';
|
||||
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 { AlertTemplatesDTO } from 'models/alert_templates';
|
||||
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';
|
||||
|
|
@ -16,13 +26,14 @@ import styles from './TemplatesAlertGroupsList.module.css';
|
|||
const cx = cn.bind(styles);
|
||||
|
||||
interface TemplatesAlertGroupsListProps {
|
||||
templates: AlertTemplatesDTO[];
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
onSelectAlertGroup?: (alertGroup: Alert) => void;
|
||||
onEditPayload?: (payload: string) => void;
|
||||
}
|
||||
|
||||
const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
||||
const { alertReceiveChannelId, onEditPayload, onSelectAlertGroup } = props;
|
||||
const { alertReceiveChannelId, templates, onEditPayload, onSelectAlertGroup } = props;
|
||||
const store = useStore();
|
||||
const [alertGroupsList, setAlertGroupsList] = useState(undefined);
|
||||
const [selectedAlertPayload, setSelectedAlertPayload] = useState<string>(undefined);
|
||||
|
|
@ -78,12 +89,15 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
<MonacoJinja2Editor
|
||||
<MonacoEditor
|
||||
value={JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
data={undefined}
|
||||
data={templates}
|
||||
height={'85vh'}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
monacoOptions={MONACO_PAYLOAD_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -102,9 +116,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 +148,16 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
<MonacoJinja2Editor
|
||||
<MonacoEditor
|
||||
value={null}
|
||||
data={undefined}
|
||||
disabled={true}
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
data={templates}
|
||||
monacoOptions={MONACO_PAYLOAD_OPTIONS}
|
||||
showLineNumbers={false}
|
||||
height={'85vh'}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -150,30 +178,37 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
{alertGroupsList?.length > 0 ? (
|
||||
{alertGroupsList ? (
|
||||
<>
|
||||
{alertGroupsList.map((alertGroup) => {
|
||||
return (
|
||||
<div key={alertGroup.pk}>
|
||||
<Button fill="text" onClick={() => getAlertGroupPayload(alertGroup.pk)}>
|
||||
{getAlertGroupName(alertGroup)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{alertGroupsList?.length > 0 ? (
|
||||
<>
|
||||
{alertGroupsList.map((alertGroup) => {
|
||||
return (
|
||||
<div key={alertGroup.pk}>
|
||||
<Button fill="text" onClick={() => getAlertGroupPayload(alertGroup.pk)}>
|
||||
{getAlertGroupName(alertGroup)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Badge
|
||||
color="blue"
|
||||
text={
|
||||
<div className={cx('no-alert-groups-badge')}>
|
||||
<Icon name="info-circle" />
|
||||
<Text>
|
||||
This integration did not receive any alerts. Use custom payload example to preview
|
||||
results.
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Badge
|
||||
color="blue"
|
||||
text={
|
||||
<HorizontalGroup>
|
||||
<Icon name="info-circle" />
|
||||
<Text>
|
||||
This integration did not receive any alerts. Use custom payload example to preview results.
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
/>
|
||||
<LoadingPlaceholder text="Loading alert groups..." />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -183,11 +183,11 @@ export const HeartIcon = (_props: IconProps) => (
|
|||
<g>
|
||||
<path
|
||||
d="M222.5,453.7c6.1,6.1,14.3,9.5,22.9,9.5c8.5,0,16.9-3.5,22.9-9.5L448,274c27.3-27.3,42.3-63.6,42.4-102.1
|
||||
c0-38.6-15-74.9-42.3-102.2S384.6,27.4,346,27.4c-37.9,0-73.6,14.5-100.7,40.9c-27.2-26.5-63-41.1-101-41.1
|
||||
c-38.5,0-74.7,15-102,42.2C15,96.7,0,133,0,171.6c0,38.5,15.1,74.8,42.4,102.1L222.5,453.7z M59.7,86.8
|
||||
c22.6-22.6,52.7-35.1,84.7-35.1s62.2,12.5,84.9,35.2l7.4,7.4c2.3,2.3,5.4,3.6,8.7,3.6l0,0c3.2,0,6.4-1.3,8.7-3.6l7.2-7.2
|
||||
c22.7-22.7,52.8-35.2,84.9-35.2c32,0,62.1,12.5,84.7,35.1c22.7,22.7,35.1,52.8,35.1,84.8s-12.5,62.1-35.2,84.8L251,436.4
|
||||
c-2.9,2.9-8.2,2.9-11.2,0l-180-180c-22.7-22.7-35.2-52.8-35.2-84.8C24.6,139.6,37.1,109.5,59.7,86.8z"
|
||||
c0-38.6-15-74.9-42.3-102.2S384.6,27.4,346,27.4c-37.9,0-73.6,14.5-100.7,40.9c-27.2-26.5-63-41.1-101-41.1
|
||||
c-38.5,0-74.7,15-102,42.2C15,96.7,0,133,0,171.6c0,38.5,15.1,74.8,42.4,102.1L222.5,453.7z M59.7,86.8
|
||||
c22.6-22.6,52.7-35.1,84.7-35.1s62.2,12.5,84.9,35.2l7.4,7.4c2.3,2.3,5.4,3.6,8.7,3.6l0,0c3.2,0,6.4-1.3,8.7-3.6l7.2-7.2
|
||||
c22.7-22.7,52.8-35.2,84.9-35.2c32,0,62.1,12.5,84.7,35.1c22.7,22.7,35.1,52.8,35.1,84.8s-12.5,62.1-35.2,84.8L251,436.4
|
||||
c-2.9,2.9-8.2,2.9-11.2,0l-180-180c-22.7-22.7-35.2-52.8-35.2-84.8C24.6,139.6,37.1,109.5,59.7,86.8z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -109,27 +109,6 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
|
||||
@action
|
||||
async updateItems(query: any = '') {
|
||||
// const filters = typeof query === 'string' ? { search: query } : query;
|
||||
// const { search } = filters;
|
||||
// const { count, results } = await makeRequest(this.path, { params: { search, page } });
|
||||
|
||||
// this.items = {
|
||||
// ...this.items,
|
||||
// ...results.reduce(
|
||||
// (acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
|
||||
// ...acc,
|
||||
// [item.id]: omit(item, 'heartbeat'),
|
||||
// }),
|
||||
// {}
|
||||
// ),
|
||||
// };
|
||||
|
||||
// this.searchResult = result.map((item: AlertReceiveChannel) => item.id);
|
||||
// this.searchResult = {
|
||||
// count,
|
||||
// results: results.map((item: AlertReceiveChannel) => item.id),
|
||||
// };
|
||||
|
||||
const params = typeof query === 'string' ? { search: query } : query;
|
||||
|
||||
const result = await makeRequest(this.path, { params });
|
||||
|
|
@ -251,7 +230,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 +418,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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -35,11 +34,7 @@ interface ExpandedIntegrationRouteDisplayProps {
|
|||
routeIndex: number;
|
||||
templates: AlertTemplatesDTO[];
|
||||
openEditTemplateModal: (templateName: string | string[], channelFilterId?: ChannelFilter['id']) => void;
|
||||
onEditRegexpTemplate: (
|
||||
templateRegexpBody: string,
|
||||
templateJijja2Body: string,
|
||||
channelFilterId: ChannelFilter['id']
|
||||
) => void;
|
||||
onEditRegexpTemplate: (channelFilterId: ChannelFilter['id']) => void;
|
||||
}
|
||||
|
||||
interface ExpandedIntegrationRouteDisplayState {
|
||||
|
|
@ -71,6 +66,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 +79,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 +101,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 +179,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 && (
|
||||
|
|
@ -242,7 +249,7 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
|
|||
|
||||
function handleEditRoutingTemplate(channelFilter, channelFilterId) {
|
||||
if (channelFilter.filtering_term_type === 0) {
|
||||
onEditRegexpTemplate(channelFilter.filtering_term, channelFilter.filtering_term_as_jinja2, channelFilterId);
|
||||
onEditRegexpTemplate(channelFilterId);
|
||||
} else {
|
||||
openEditTemplateModal('route_template', channelFilterId);
|
||||
}
|
||||
|
|
@ -272,11 +279,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 +291,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 +299,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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { HeartIcon, HeartRedIcon } from 'icons';
|
||||
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,30 +354,25 @@ 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]
|
||||
}
|
||||
templates={templates}
|
||||
/>
|
||||
)}
|
||||
{isEditRegexpRouteTemplateModalOpen && (
|
||||
|
|
@ -416,19 +391,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 +444,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,13 +485,20 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
|
|||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
|
||||
if (
|
||||
!alertReceiveChannel.is_available_for_integration_heartbeat ||
|
||||
alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal === null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipBadge
|
||||
text={undefined}
|
||||
className={cx('heartbeat-badge')}
|
||||
borderType={alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal ? 'success' : 'danger'}
|
||||
customIcon={heartbeatStatus ? <HeartGreenIcon /> : <HeartRedIcon />}
|
||||
tooltipTitle={`Last heartbeat: ${alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal || 'never'}`}
|
||||
borderType={heartbeatStatus ? 'success' : 'danger'}
|
||||
customIcon={heartbeatStatus ? <HeartIcon /> : <HeartRedIcon />}
|
||||
tooltipTitle={`Last heartbeat: ${alertReceiveChannel.heartbeat?.last_heartbeat_time_verbal}`}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
);
|
||||
|
|
@ -507,38 +526,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 +539,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 +579,14 @@ 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 });
|
||||
if (templateForEdit[templateName]) {
|
||||
this.setState({
|
||||
isEditTemplateModalOpen: true,
|
||||
selectedTemplate: templateForEdit[templateName],
|
||||
});
|
||||
} else {
|
||||
openErrorNotification('Template can not be edited. Please contact support.');
|
||||
}
|
||||
|
||||
if (channelFilterId) {
|
||||
this.setState({ channelFilterIdForEdit: channelFilterId });
|
||||
|
|
@ -609,14 +602,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 +672,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 +688,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 +729,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 +747,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 +966,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 +993,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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import IntegrationForm2 from 'containers/IntegrationForm/IntegrationForm2';
|
|||
import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { HeartGreenIcon, HeartRedIcon } from 'icons';
|
||||
import { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel';
|
||||
import { HeartIcon, HeartRedIcon } from 'icons';
|
||||
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';
|
||||
|
|
@ -36,6 +36,7 @@ import styles from './Integrations2.module.scss';
|
|||
const cx = cn.bind(styles);
|
||||
const FILTERS_DEBOUNCE_MS = 500;
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
const MAX_LINE_LENGTH = 40;
|
||||
|
||||
interface IntegrationsState extends PageBaseState {
|
||||
integrationsFilters: Filters;
|
||||
|
|
@ -116,7 +117,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
|
||||
const columns = [
|
||||
{
|
||||
width: '25%',
|
||||
width: '35%',
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
render: this.renderName,
|
||||
|
|
@ -129,7 +130,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
render: (item: AlertReceiveChannel) => this.renderIntegrationStatus(item, alertReceiveChannelStore),
|
||||
},
|
||||
{
|
||||
width: '25%',
|
||||
width: '20%',
|
||||
title: 'Datasource',
|
||||
key: 'datasource',
|
||||
render: (item: AlertReceiveChannel) => this.renderDatasource(item, alertReceiveChannelStore),
|
||||
|
|
@ -147,7 +148,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
render: (item: AlertReceiveChannel) => this.renderHeartbeat(item, alertReceiveChannelStore, heartbeatStore),
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
width: '15%',
|
||||
title: 'Team',
|
||||
render: (item: AlertReceiveChannel) => this.renderTeam(item, grafanaTeamStore.items),
|
||||
},
|
||||
|
|
@ -229,7 +230,14 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
return (
|
||||
<PluginLink query={{ page: 'integrations_2', id: item.id }}>
|
||||
<Text type="link" size="medium">
|
||||
<Emoji className={cx('title')} text={item.verbal_name} />
|
||||
<Emoji
|
||||
className={cx('title')}
|
||||
text={
|
||||
item.verbal_name.length > MAX_LINE_LENGTH
|
||||
? item.verbal_name.substring(0, MAX_LINE_LENGTH) + '...'
|
||||
: item.verbal_name
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
</PluginLink>
|
||||
);
|
||||
|
|
@ -292,13 +300,13 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
return (
|
||||
<div>
|
||||
{alertReceiveChannel.is_available_for_integration_heartbeat && (
|
||||
{alertReceiveChannel.is_available_for_integration_heartbeat && heartbeat?.last_heartbeat_time_verbal && (
|
||||
<TooltipBadge
|
||||
text={undefined}
|
||||
className={cx('heartbeat-badge')}
|
||||
borderType={heartbeat?.last_heartbeat_time_verbal ? 'success' : 'danger'}
|
||||
customIcon={heartbeatStatus ? <HeartGreenIcon /> : <HeartRedIcon />}
|
||||
tooltipTitle={`Last heartbeat: ${heartbeat?.last_heartbeat_time_verbal || 'never'}`}
|
||||
borderType={heartbeatStatus ? 'success' : 'danger'}
|
||||
customIcon={heartbeatStatus ? <HeartIcon /> : <HeartRedIcon />}
|
||||
tooltipTitle={`Last heartbeat: ${heartbeat?.last_heartbeat_time_verbal}`}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -337,7 +345,16 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
<IconButton tooltip="Settings" name="cog" onClick={() => this.onIntegrationEditClick(item.id)} />
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip key="edit" userAction={UserActions.IntegrationsWrite}>
|
||||
<WithConfirm>
|
||||
<WithConfirm
|
||||
description={
|
||||
<Text>
|
||||
<Emoji
|
||||
className={cx('title')}
|
||||
text={`Are you sure you want to delete ${item.verbal_name} integration?`}
|
||||
/>
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
tooltip="Delete"
|
||||
name="trash-alt"
|
||||
|
|
@ -368,9 +385,9 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
applyFilters = () => {
|
||||
const { store } = this.props;
|
||||
const { alertReceiveChannelStore } = store;
|
||||
const { integrationsFilters } = this.state;
|
||||
const { integrationsFilters, page } = this.state;
|
||||
|
||||
return alertReceiveChannelStore.updateItems(integrationsFilters);
|
||||
return alertReceiveChannelStore.updatePaginatedItems(integrationsFilters, page);
|
||||
};
|
||||
|
||||
debouncedUpdateIntegrations = debounce(this.applyFilters, FILTERS_DEBOUNCE_MS);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ interface MaintenancePageState {
|
|||
maintenanceData?: {
|
||||
type?: MaintenanceType;
|
||||
alert_receive_channel_id?: AlertReceiveChannel['id'];
|
||||
disabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue