Templates&grouping changes (#2291)

# What this PR does

- Fixed newlines in cheatsheat display
- Reference templates by keys instead of display names
- UI esthetic changes asked by Raphael (borders, sizing, spacing etc)
- Increased Demo Alert Modal height
- Moved MoveUp/MoveDown on routes to ellipsis on the right
- Hide Heartbeat Settings if there's none to display
- Removed Telegram request as now the data is available in response
This commit is contained in:
Rares Mardare 2023-06-20 19:03:07 +03:00 committed by GitHub
parent e4788d5732
commit bc32726fbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 420 additions and 292 deletions

View file

@ -1,5 +1,4 @@
import { TemplateForEdit, commonTemplateForEdit } from './CommonAlertTemplatesForm.config';
export interface Template {
name: string;
group: string;

View file

@ -1,3 +1,5 @@
import { TemplateOptions } from 'pages/integration_2/Integration2.config';
export interface Template {
name: string;
group: string;
@ -9,6 +11,7 @@ export interface TemplateForEdit {
description?: string;
additionalData?: {
chatOpsName?: string;
chatOpsDisplayName?: string;
data?: string;
additionalDescription?: string;
};
@ -18,17 +21,17 @@ export interface TemplateForEdit {
export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
web_title_template: {
displayName: 'Web title',
name: 'web_title_template',
name: TemplateOptions.WebTitle.key,
description: '',
},
web_message_template: {
displayName: 'Web message',
name: 'web_message_template',
name: TemplateOptions.WebMessage.key,
description: '',
},
slack_title_template: {
name: 'slack_title_template',
displayName: 'Slack title',
displayName: TemplateOptions.SlackTitle.key,
description: '',
additionalData: {
chatOpsName: 'slack',
@ -36,22 +39,22 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
},
},
sms_title_template: {
name: 'sms_title_template',
name: TemplateOptions.SMS.key,
displayName: 'Sms title',
description: '',
},
phone_call_title_template: {
name: 'phone_call_title_template',
name: TemplateOptions.Phone.key,
displayName: 'Phone call title',
description: '',
},
email_title_template: {
name: 'email_title_template',
name: TemplateOptions.EmailTitle.key,
displayName: 'Email title',
description: '',
},
telegram_title_template: {
name: 'telegram_title_template',
name: TemplateOptions.TelegramTitle.key,
displayName: 'Telegram title',
description: '',
additionalData: {
@ -59,7 +62,7 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
},
},
slack_message_template: {
name: 'slack_message_template',
name: TemplateOptions.SlackMessage.key,
displayName: 'Slack message',
description: '',
additionalData: {
@ -68,12 +71,12 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
},
},
email_message_template: {
name: 'email_message_template',
name: TemplateOptions.EmailMessage.key,
displayName: 'Email message',
description: '',
},
telegram_message_template: {
name: 'telegram_message_template',
name: TemplateOptions.TelegramMessage.key,
displayName: 'Telegram message',
description: '',
additionalData: {
@ -81,7 +84,7 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
},
},
slack_image_url_template: {
name: 'slack_image_url_template',
name: TemplateOptions.SlackImage.key,
displayName: 'Slack image url',
description: '',
additionalData: {
@ -90,12 +93,12 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
},
},
web_image_url_template: {
name: 'web_image_url_template',
name: TemplateOptions.WebImage.key,
displayName: 'Web image url',
description: '',
},
telegram_image_url_template: {
name: 'telegram_image_url_template',
name: TemplateOptions.TelegramImage.key,
displayName: 'Telegram image url',
description: '',
additionalData: {
@ -103,32 +106,29 @@ export const commonTemplateForEdit: { [id: string]: TemplateForEdit } = {
},
},
grouping_id_template: {
name: 'grouping_id_template',
name: TemplateOptions.Grouping.key,
displayName: 'Grouping',
description:
'Reduce noise, minimize duplication with Alert Grouping, based on time, alert content, and even multiple features at the same time. Check the cheasheet to customize your template.',
additionalData: {
additionalDescription: 'Alerts with this Grouping ID are grouped together',
},
},
acknowledge_condition_template: {
name: 'acknowledge_condition_template',
name: TemplateOptions.Autoacknowledge.key,
displayName: 'Acknowledge condition',
description: '',
},
resolve_condition_template: {
name: 'resolve_condition_template',
name: TemplateOptions.Resolve.key,
displayName: 'Resolve condition',
description:
'When monitoring systems return to normal, they can send "resolve" alerts. If Autoresolution Template is True, the alert will resolve its group as "resolved by source". If the group is already resolved, the alert will be added to that group',
'When monitoring systems return to normal, they can send "resolve" alerts. OnCall can use these signals to resolve alert groups accordingly.',
},
source_link_template: {
name: 'source_link_template',
name: TemplateOptions.SourceLink.key,
displayName: 'Source link',
description: '',
},
route_template: {
name: 'route_template',
name: TemplateOptions.Routing.key,
displayName: 'Routing',
description:
'Routes direct alerts to different escalation chains based on the content, such as severity or region.',

View file

@ -47,7 +47,12 @@ export const genericTemplateCheatSheet: CheatSheetInterface = {
{
name: 'Markdown refresher',
listItems: [
{ codeExample: '**bold**, _italic_, >quote, `code`, ```multiline code```, [``](url), - bullet list' },
{
codeExample: `**bold**, _italic_, >quote, \`code\`,
\`\`\`multiline code\`\`\`
<slug|url>
- bullet list`,
},
],
},
{
@ -56,10 +61,17 @@ export const genericTemplateCheatSheet: CheatSheetInterface = {
{ listItemName: ' {{ payload.labels.foo }} - extract field value' },
{
listItemName: 'Conditions',
codeExample: '{%- if "status" in payload %} \n {{ payload.status }} \n {% endif -%}',
codeExample: `{%- if "status" in payload %}
{{ payload.status }}
{% endif -%}`,
},
{ listItemName: 'Booleans', codeExample: '{{ payload.status == "resolved" }}' },
{
listItemName: 'Loops',
codeExample: `{% for label in labels %}
{{ label.title }}
{% endfor %}`,
},
{ listItemName: 'Booleans', codeExample: '{{ payload.status == “resolved” }}' },
{ listItemName: 'Loops', codeExample: '{% for label in labels %} \n {{ label.title }} \n {% endfor %}' },
],
},
{
@ -132,11 +144,17 @@ export const slackMessageTemplateCheatSheet: CheatSheetInterface = {
listItems: [
{
listItemName: 'Examples Convert Web template in Classic Markdown to Slack markdown',
codeExample: '{{ web_message \n| replace("**", "*") \n| regex_replace("/((.*))[(.*)]/", "<$2|$1>") }}',
codeExample: `{{
web_message
| replace("**", "*")
| regex_replace("/((.*))[(.*)]/", "<$2|$1>")
}}`,
},
{
listItemName: 'Show status if exists',
codeExample: '{%- if "status" in payload %} \n **Status**: {{ payload.status }} \n {% endif -%}',
codeExample: `{%- if "status" in payload %}
**Status**: {{ payload.status }}
{% endif -%}`,
},
{
listItemName: 'Show field value or “N/A” is not exist',
@ -144,8 +162,10 @@ export const slackMessageTemplateCheatSheet: CheatSheetInterface = {
},
{
listItemName: 'Iterate over labels dictionary',
codeExample:
'**Labels:** \n {% for k, v in payload["labels"].items() %} \n *{{ k }}*: {{ v }} \n {% endfor %} ',
codeExample: `**Labels:**
{% for k, v in payload["labels"].items() %}
*{{ k }}*: {{ v }}
{% endfor %}`,
},
],
},

View file

@ -2,16 +2,23 @@
width: 40%;
height: 100%;
padding: 16px;
padding-right: 0;
border: var(--border-weak);
min-width: min-content;
overflow-y: scroll;
}
.cheatsheet-container > div {
.cheatsheet-innerContainer {
padding-right: 16px;
overflow-y: scroll;
height: 100%;
max-height: 100%;
> div {
height: 100%;
max-height: 100%;
}
}
.cheatsheet-item {
margin-bottom: 24px;
}
@ -20,3 +27,7 @@
margin-bottom: 16px;
width: 100%;
}
.code {
white-space: pre;
}

View file

@ -9,8 +9,7 @@ import Text from 'components/Text/Text';
import { openNotification } from 'utils';
import { CheatSheetInterface, CheatSheetItem } from './CheatSheet.config';
import styles from './CheatSheet.module.css';
import styles from './CheatSheet.module.scss';
interface CheatSheetProps {
cheatSheetName: string;
@ -24,22 +23,24 @@ const CheatSheet = (props: CheatSheetProps) => {
const { cheatSheetName, cheatSheetData, onClose } = props;
return (
<div className={cx('cheatsheet-container')}>
<VerticalGroup>
<HorizontalGroup justify="space-between">
<Text strong>{cheatSheetName} cheatsheet</Text>
<IconButton name="times" onClick={onClose} />
</HorizontalGroup>
<Text type="secondary">{cheatSheetData.description}</Text>
<div style={{ width: '100%' }}>
{cheatSheetData.fields?.map((field: CheatSheetItem) => {
return (
<div key={field.name} className={cx('cheatsheet-item')}>
<CheatSheetListItem field={field} />
</div>
);
})}
</div>
</VerticalGroup>
<div className={cx('cheatsheet-innerContainer')}>
<VerticalGroup>
<HorizontalGroup justify="space-between">
<Text strong>{cheatSheetName} cheatsheet</Text>
<IconButton name="times" onClick={onClose} />
</HorizontalGroup>
<Text type="secondary">{cheatSheetData.description}</Text>
<div className={cx('u-width-100')}>
{cheatSheetData.fields?.map((field: CheatSheetItem) => {
return (
<div key={field.name} className={cx('cheatsheet-item')}>
<CheatSheetListItem field={field} />
</div>
);
})}
</div>
</VerticalGroup>
</div>
</div>
);
};
@ -65,7 +66,9 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => {
<div className={cx('cheatsheet-item-small')}>
<Block bordered fullWidth withBackground>
<HorizontalGroup justify="space-between">
<Text type="link">{item.codeExample}</Text>
<Text type="link" className={cx('code')}>
{item.codeExample}
</Text>
<CopyToClipboard text={item.codeExample} onCopy={() => openNotification('Example copied')}>
<IconButton name="copy" />
</CopyToClipboard>

View file

@ -5,17 +5,24 @@
.integrationBlock__heading {
background-color: var(--background-secondary);
border: none;
border: var(--border-medium) !important;
border-radius: 0 !important;
border-top-left-radius: 4px !important;
border-top-right-radius: 4px !important;
}
.integrationBlock__content {
background: var(--background-primary);
border: var(--border-weak);
border: var(--border-medium) !important;
border-top: none !important;
padding-bottom: 0;
border-bottom-left-radius: 4px !important;
border-bottom-right-radius: 4px !important;
&--collapsedBorder {
border-left: none;
padding-left: 0;
padding-right: 0;
padding-bottom: 24px;
}
}

View file

@ -3,6 +3,7 @@
flex-direction: row;
margin-bottom: 4px;
max-width: 100%;
margin-left: 16px;
&__content {
width: 100%;

View file

@ -53,7 +53,7 @@ const IntegrationTemplateBlock: React.FC<IntegrationTemplateBlockProps> = ({
</Tooltip>
</WithPermissionControlTooltip>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Tooltip content={'Reset Template to default'}>
<Tooltip content={'Reset template to default'}>
<Button variant={'secondary'} icon={'times'} size={'md'} onClick={onRemove} />
</Tooltip>
</WithPermissionControlTooltip>

View file

@ -18,7 +18,7 @@
display: flex;
white-space: nowrap;
flex-direction: row;
gap: 8px;
gap: 4px;
}
&__item--large {
@ -32,3 +32,21 @@
text-overflow: ellipsis;
}
}
.icon {
margin-right: 4px;
}
.collapsedRoute {
&__container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
&__item {
display: flex;
flex-direction: row;
}
}

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { ConfirmModal, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
import { ConfirmModal, HorizontalGroup, Icon } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
@ -63,6 +63,7 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
routeIndex
)}
className={cx('u-margin-right-xs')}
tooltipContent={undefined}
/>
{routeWording === 'Default' && <Text type="secondary">Unmatched alerts routed to default route</Text>}
@ -95,29 +96,31 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
}
content={
<div className={cx('spacing')}>
<VerticalGroup>
<div className={cx('collapsedRoute__container')}>
{chatOpsAvailableChannels.length > 0 && (
<HorizontalGroup spacing="xs">
<Text type="secondary">Publish to ChatOps</Text>
<div className={cx('collapsedRoute__item')}>
<HorizontalGroup spacing="xs">
<Text type="secondary">Publish to ChatOps</Text>
{chatOpsAvailableChannels
.filter((it) => it)
.map((chatOpsChannel) => (
<>
<Icon name={chatOpsChannel.icon} />
<Text type="primary" strong>
{chatOpsChannel.name}
</Text>
</>
))}
</HorizontalGroup>
{chatOpsAvailableChannels
.filter((it) => it)
.map((chatOpsChannel) => (
<>
<Icon name={chatOpsChannel.icon} className={cx('icon')} />
<Text type="primary">{chatOpsChannel.name}</Text>
</>
))}
</HorizontalGroup>
</div>
)}
<HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
<div className={cx('collapsedRoute__item')}>
<div className={cx('u-flex', 'u-align-items-center', 'u-flex-xs')}>
<Icon name="list-ui-alt" />
<Text type="secondary">Trigger escalation chain</Text>
</HorizontalGroup>
<Text type="secondary" className={cx('u-margin-right-xs')}>
Trigger escalation chain
</Text>
</div>
{escalationChain?.name && (
<PluginLink
@ -130,15 +133,15 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
)}
{!escalationChain?.name && (
<HorizontalGroup spacing={'xs'}>
<div className={cx('u-flex', 'u-align-items-center', 'u-flex-xs')}>
<div className={cx('icon-exclamation')}>
<Icon name="exclamation-triangle" />
</div>
<Text type="primary">Not selected</Text>
</HorizontalGroup>
</div>
)}
</HorizontalGroup>
</VerticalGroup>
</div>
</div>
</div>
}
/>

View file

@ -33,8 +33,8 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_
import { AlertTemplatesDTO } from 'models/alert_templates';
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import CommonIntegrationHelper from 'pages/integration_2/CommonIntegration2.helper';
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_OPTIONS } from 'pages/integration_2/Integration2.config';
import IntegrationHelper from 'pages/integration_2/Integration2.helper';
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_OPTIONS } from 'pages/integration_2/Integration2Common.config';
import { useStore } from 'state/useStore';
import { openNotification } from 'utils';
import { UserActions } from 'utils/authorization';
@ -121,6 +121,7 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
text={CommonIntegrationHelper.getRouteConditionWording(channelFilterIds, routeIndex)}
tooltipTitle={CommonIntegrationHelper.getRouteConditionTooltipWording(channelFilterIds, routeIndex)}
tooltipContent={undefined}
className={cx('u-margin-right-xs')}
/>
</HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
@ -323,26 +324,36 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
return (
<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={'sm'} />
</Tooltip>
</WithPermissionControlTooltip>
)}
{routeIndex < channelFilterIds.length - 2 && !channelFilter.is_default && (
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Tooltip placement="top" content={'Move Down'}>
<Button variant={'secondary'} onClick={onRouteMoveDown} icon={'arrow-down'} size={'sm'} />
</Tooltip>
</WithPermissionControlTooltip>
)}
{!channelFilter.is_default && (
<WithContextMenu
renderMenuItems={() => (
<div className={cx('integrations-actionsList')}>
{routeIndex > 0 && !channelFilter.is_default && (
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<div className={cx('integrations-actionItem')}>
<HorizontalGroup spacing="xs">
<Icon name="arrow-up" />
<Text type="primary" onClick={onRouteMoveUp}>
Move Up
</Text>
</HorizontalGroup>
</div>
</WithPermissionControlTooltip>
)}
{routeIndex < channelFilterIds.length - 2 && !channelFilter.is_default && (
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<div className={cx('integrations-actionItem')}>
<HorizontalGroup spacing="xs">
<Icon name={'arrow-down'} />
<Text type="primary" onClick={onRouteMoveDown}>
Move Down
</Text>
</HorizontalGroup>
</div>
</WithPermissionControlTooltip>
)}
<CopyToClipboard text={channelFilter.id} onCopy={() => openNotification('Route ID is copied')}>
<div className={cx('integrations-actionItem')}>
<HorizontalGroup spacing={'xs'}>

View file

@ -1,4 +1,4 @@
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_INPUT_HEIGHT_TALL } from 'pages/integration_2/Integration2.config';
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_INPUT_HEIGHT_TALL } from 'pages/integration_2/Integration2Common.config';
interface TemplateToRender {
name: string;

View file

@ -10,9 +10,9 @@ import Text from 'components/Text/Text';
import { templatesToRender } from 'containers/IntegrationContainers/IntegrationTemplatesList.config';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { AlertTemplatesDTO } from 'models/alert_templates';
import { MONACO_INPUT_HEIGHT_TALL, MONACO_OPTIONS } from 'pages/integration_2/Integration2.config';
import IntegrationHelper from 'pages/integration_2/Integration2.helper';
import styles from 'pages/integration_2/Integration2.module.scss';
import { MONACO_INPUT_HEIGHT_TALL, MONACO_OPTIONS } from 'pages/integration_2/Integration2Common.config';
import { useStore } from 'state/useStore';
import { openErrorNotification, openNotification } from 'utils';
@ -90,11 +90,15 @@ const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
<>
{isResolveConditionTemplate(contents.name) && (
<Tooltip content={'Edit'}>
<InlineSwitch value={autoresolveValue} onChange={handleSaveClick} />
<InlineSwitch
value={autoresolveValue}
onChange={handleSaveClick}
className={cx('inline-switch')}
/>
</Tooltip>
)}
{isResolveConditionTemplateEditable(contents.name) && (
<div className={cx('input')}>
<div className={cx('input', { 'input-with-toggle': isResolveConditionTemplate(contents.name) })}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(
templates[contents.name] || '',

View file

@ -14,6 +14,7 @@
max-height: 100%;
width: 100%;
border: var(--border-strong);
padding-left: 16px;
}
.template-block-title {
@ -53,7 +54,9 @@
.template-block-result {
width: 30%;
overflow-y: scroll !important;
padding-right: 16px;
}
.result {
padding: 0 16px;
padding-bottom: 60px;

View file

@ -22,6 +22,7 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_
import { AlertTemplatesDTO } from 'models/alert_templates';
import { Alert } from 'models/alertgroup/alertgroup.types';
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { TemplateOptions } from 'pages/integration_2/Integration2.config';
import { waitForElement } from 'utils/DOM';
import LocationHelper from 'utils/LocationHelper';
import { UserActions } from 'utils/authorization';
@ -124,27 +125,27 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
}
}, [onUpdateTemplates, changedTemplateBody]);
const getCheatSheet = (templateName) => {
switch (templateName) {
case 'Grouping':
case 'Autoresolve':
const getCheatSheet = (templateKey: string) => {
switch (templateKey) {
case TemplateOptions.Grouping.key:
case TemplateOptions.Resolve.key:
return groupingTemplateCheatSheet;
case 'Web title':
case 'Web message':
case 'Web image':
case TemplateOptions.WebTitle.key:
case TemplateOptions.WebMessage.key:
case TemplateOptions.WebImage.key:
return genericTemplateCheatSheet;
case 'Auto acknowledge':
case 'Source link':
case 'Phone call':
case 'SMS':
case 'Slack title':
case 'Slack message':
case 'Slack image':
case 'Telegram title':
case 'Telegram message':
case 'Telegram image':
case 'Email title':
case 'Email message':
case TemplateOptions.Autoacknowledge.key:
case TemplateOptions.SourceLink.key:
case TemplateOptions.Phone.key:
case TemplateOptions.SMS.key:
case TemplateOptions.SlackTitle.key:
case TemplateOptions.SlackMessage.key:
case TemplateOptions.SlackImage.key:
case TemplateOptions.TelegramTitle.key:
case TemplateOptions.TelegramMessage.key:
case TemplateOptions.TelegramImage.key:
case TemplateOptions.EmailTitle.key:
case TemplateOptions.EmailMessage.key:
return slackMessageTemplateCheatSheet;
default:
return genericTemplateCheatSheet;
@ -190,7 +191,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
{isCheatSheetVisible ? (
<CheatSheet
cheatSheetName={template.displayName}
cheatSheetData={getCheatSheet(template.displayName)}
cheatSheetData={getCheatSheet(template.name)}
onClose={onCloseCheatSheet}
/>
) : (
@ -201,7 +202,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
<Text>Template editor</Text>
<Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
Cheatsheat
Cheatsheet
</Button>
</HorizontalGroup>
</div>
@ -249,9 +250,6 @@ const Result = (props: ResultProps) => {
const { alertReceiveChannelId, template, templateBody, chatOpsPermalink, payload, error, onSaveAndFollowLink } =
props;
const getCapitalizedChatopsName = (name: string) => {
return name.charAt(0).toUpperCase() + name.slice(1);
};
return (
<div className={cx('template-block-result')}>
<div className={cx('template-block-title')}>
@ -286,7 +284,7 @@ const Result = (props: ResultProps) => {
<VerticalGroup>
<Button onClick={() => onSaveAndFollowLink(chatOpsPermalink)}>
<HorizontalGroup spacing="xs" align="center">
Save and open Alert Group in {getCapitalizedChatopsName(template.additionalData.chatOpsName)}{' '}
Save and open Alert Group in {template.additionalData.chatOpsDisplayName}{' '}
<Icon name="external-link-alt" />
</HorizontalGroup>
</Button>

View file

@ -10,7 +10,7 @@ import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
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 { MONACO_OPTIONS, MONACO_PAYLOAD_OPTIONS } from 'pages/integration_2/Integration2Common.config';
import { useStore } from 'state/useStore';
import styles from './TemplatesAlertGroupsList.module.css';
@ -129,6 +129,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
className={cx('alert-groups-last-payload-badge')}
/>
<div className={cx('alert-groups-editor-withBadge')}>
{/* Editor used for Editing Given Payload */}
<MonacoEditor
value={JSON.stringify(selectedAlertPayload, null, 4)}
data={undefined}
@ -167,8 +168,10 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
useAutoCompleteList={false}
language={MONACO_LANGUAGE.json}
data={templates}
monacoOptions={MONACO_PAYLOAD_OPTIONS}
showLineNumbers
monacoOptions={{
...MONACO_OPTIONS,
readOnly: false,
}}
height={getCodeEditorHeight()}
onChange={getChangeHandler()}
/>

View file

@ -1,7 +1,7 @@
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
import { TelegramChannel } from 'models/telegram_channel/telegram_channel.types';
import { TelegramChannel, TelegramChannelDetails } from 'models/telegram_channel/telegram_channel.types';
export enum FilteringTermType {
regex,
@ -15,6 +15,7 @@ export interface ChannelFilter {
slack_channel_id?: SlackChannel['id'];
slack_channel?: SlackChannel;
telegram_channel?: TelegramChannel['id'];
telegram_channel_details?: TelegramChannelDetails;
created_at: string;
filtering_term: string;
filtering_term_as_jinja2: string;

View file

@ -6,3 +6,8 @@ export interface TelegramChannel {
discussion_group_name: string;
is_default_channel: false;
}
export interface TelegramChannelDetails {
display_name: string;
id: string;
}

View file

@ -3,125 +3,12 @@
Any change to this file needs to be done in the oncall-private also
*/
import { KeyValuePair } from 'utils';
import { BASE_INTEGRATION_TEMPLATES_LIST, BaseTemplateOptions } from './Integration2Common.config';
export const TEXTAREA_ROWS_COUNT = 4;
export const MAX_CHARACTERS_COUNT = 50;
export const MONACO_OPTIONS = {
renderLineHighlight: false,
readOnly: true,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
verticalScrollbarSize: 0,
handleMouseWheel: false,
},
hideCursorInOverviewRuler: true,
minimap: { enabled: false },
cursorStyle: {
display: 'none',
},
export const TemplateOptions = {
...BaseTemplateOptions,
};
export const MONACO_PAYLOAD_OPTIONS = {
renderLineHighlight: false,
readOnly: false,
hideCursorInOverviewRuler: true,
minimap: { enabled: false },
cursorStyle: {
display: 'none',
},
export const INTEGRATION_TEMPLATES_LIST = {
...BASE_INTEGRATION_TEMPLATES_LIST,
};
export const MONACO_INPUT_HEIGHT_SMALL = '32px';
export const MONACO_INPUT_HEIGHT_TALL = '120px';
const TemplateOptions = {
SourceLink: new KeyValuePair('source_link_template', 'Source Link'),
Autoacknowledge: new KeyValuePair('acknowledge_condition_template', 'Autoacknowledge'),
Phone: new KeyValuePair('phone_call_title_template', 'Phone'),
SMS: new KeyValuePair('sms_title_template', 'SMS'),
SlackTitle: new KeyValuePair('slack_title_template', 'Title'),
SlackMessage: new KeyValuePair('slack_message_template', 'Message'),
SlackImage: new KeyValuePair('slack_image_url_template', 'Image'),
EmailTitle: new KeyValuePair('email_title_template', 'Title'),
EmailMessage: new KeyValuePair('email_message_template', 'Message'),
TelegramTitle: new KeyValuePair('telegram_title_template', 'Title'),
TelegramMessage: new KeyValuePair('telegram_message_template', 'Message'),
TelegramImage: new KeyValuePair('telegram_image_url_template', 'Image'),
Email: new KeyValuePair('Email', 'Email'),
Slack: new KeyValuePair('Slack', 'Slack'),
MSTeams: new KeyValuePair('Microsoft Teams', 'Microsoft Teams'),
Telegram: new KeyValuePair('Telegram', 'Telegram'),
};
export const INTEGRATION_TEMPLATES_LIST = [
{
label: TemplateOptions.SourceLink.value,
value: TemplateOptions.SourceLink.key,
},
{
label: TemplateOptions.Autoacknowledge.value,
value: TemplateOptions.Autoacknowledge.key,
},
{
label: TemplateOptions.Phone.value,
value: TemplateOptions.Phone.key,
},
{
label: TemplateOptions.SMS.value,
value: TemplateOptions.SMS.key,
},
{
label: TemplateOptions.Email.value,
value: TemplateOptions.Email.key,
children: [
{
label: TemplateOptions.EmailTitle.value,
value: TemplateOptions.EmailTitle.key,
},
{
label: TemplateOptions.EmailMessage.value,
value: TemplateOptions.EmailMessage.key,
},
],
},
{
label: TemplateOptions.Slack.value,
value: TemplateOptions.Slack.key,
children: [
{
label: TemplateOptions.SlackTitle.value,
value: TemplateOptions.SlackTitle.key,
},
{
label: TemplateOptions.SlackMessage.value,
value: TemplateOptions.SlackMessage.key,
},
{
label: TemplateOptions.SlackImage.value,
value: TemplateOptions.SlackImage.key,
},
],
},
{
label: TemplateOptions.Telegram.value,
value: TemplateOptions.Telegram.key,
children: [
{
label: TemplateOptions.TelegramTitle.value,
value: TemplateOptions.TelegramTitle.key,
},
{
label: TemplateOptions.TelegramMessage.value,
value: TemplateOptions.TelegramMessage.key,
},
{
label: TemplateOptions.TelegramImage.value,
value: TemplateOptions.TelegramImage.key,
},
],
},
];

View file

@ -11,7 +11,7 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { RootStore } from 'state';
import { AppFeature } from 'state/features';
import { MAX_CHARACTERS_COUNT, TEXTAREA_ROWS_COUNT } from './Integration2.config';
import { MAX_CHARACTERS_COUNT, TEXTAREA_ROWS_COUNT } from './Integration2Common.config';
const IntegrationHelper = {
getFilteredTemplate: (template: string, isTextArea: boolean): string => {
@ -63,7 +63,6 @@ const IntegrationHelper = {
getChatOpsChannels(channelFilter: ChannelFilter, store: RootStore): Array<{ name: string; icon: IconName }> {
const channels: Array<{ name: string; icon: IconName }> = [];
const telegram = Object.keys(store.telegramChannelStore.items).map((k) => store.telegramChannelStore.items[k]);
if (store.hasFeature(AppFeature.Slack) && channelFilter.notify_in_slack) {
const matchingSlackChannel = store.teamStore.currentTeam?.slack_channel?.id
@ -77,15 +76,12 @@ const IntegrationHelper = {
}
}
const matchingTelegram = telegram.find((t) => t.id === channelFilter.telegram_channel);
if (
store.hasFeature(AppFeature.Telegram) &&
channelFilter.telegram_channel &&
channelFilter.notify_in_telegram &&
matchingTelegram?.channel_name
channelFilter.telegram_channel_details &&
channelFilter.notify_in_telegram
) {
channels.push({ name: matchingTelegram.channel_name, icon: 'telegram-alt' });
channels.push({ name: channelFilter.telegram_channel_details.display_name, icon: 'telegram-alt' });
}
return channels;

View file

@ -49,7 +49,7 @@ $LARGE-MARGIN: 24px;
&__description {
display: block;
margin-bottom: $LARGE-MARGIN;
margin-bottom: 32px;;
}
&__counter {
@ -118,6 +118,9 @@ $LARGE-MARGIN: 24px;
flex-grow: 1;
max-width: calc(100% - 80px);
}
.input-with-toggler {
max-width: calc(100% - 134px);
}
.how-to-connect__container {
display: flex;
@ -205,3 +208,11 @@ $LARGE-MARGIN: 24px;
gap: 4px;
}
}
.radius {
border-radius: 4px;
}
.inline-switch {
height: 34px;
}

View file

@ -58,10 +58,9 @@ import {
import { AlertTemplatesDTO } from 'models/alert_templates';
import { ChannelFilter } from 'models/channel_filter';
import { MaintenanceType } from 'models/maintenance/maintenance.types';
import { INTEGRATION_TEMPLATES_LIST, MONACO_PAYLOAD_OPTIONS } from 'pages/integration_2/Integration2.config';
import { INTEGRATION_TEMPLATES_LIST } from 'pages/integration_2/Integration2.config';
import IntegrationHelper from 'pages/integration_2/Integration2.helper';
import styles from 'pages/integration_2/Integration2.module.scss';
import { AppFeature } from 'state/features';
import { PageProps, SelectOption, WithStoreProps } from 'state/types';
import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore';
@ -72,6 +71,8 @@ import { UserActions } from 'utils/authorization';
import { PLUGIN_ROOT } from 'utils/consts';
import sanitize from 'utils/sanitize';
import { MONACO_PAYLOAD_OPTIONS } from './Integration2Common.config';
const cx = cn.bind(styles);
interface Integration2Props extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
@ -118,15 +119,9 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
} = this.props;
const {
store,
store: { alertReceiveChannelStore, telegramChannelStore },
store: { alertReceiveChannelStore },
} = this.props;
if (store.hasFeature(AppFeature.Telegram)) {
// workaround until we get the whole telegram data in response
telegramChannelStore.updateItems();
}
if (query?.template) {
this.openEditTemplateModal(query.template, query.routeId && query.routeId);
}
@ -311,7 +306,7 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
border={getVar('--border-weak')}
className={cx('tag')}
>
<Text type="primary" size="small">
<Text type="primary" size="small" className={cx('radius')}>
Templates
</Text>
</Tag>
@ -677,7 +672,7 @@ const IntegrationSendDemoPayloadModal: React.FC<IntegrationSendDemoPayloadModalP
<MonacoEditor
value={initialDemoJSON}
disabled={true}
height={`200px`}
height={`60vh`}
useAutoCompleteList={false}
language={MONACO_LANGUAGE.json}
data={undefined}
@ -736,7 +731,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
alertReceiveChannel,
changeIsTemplateSettingsOpen,
}) => {
const { maintenanceStore, alertReceiveChannelStore } = useStore();
const { maintenanceStore, alertReceiveChannelStore, heartbeatStore } = useStore();
const history = useHistory();
@ -830,11 +825,13 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
<Text type="primary">Integration Settings</Text>
</div>
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
<div className={cx('integration__actionItem')} onClick={() => setIsHearbeatFormOpen(true)}>
Hearbeat Settings
</div>
</WithPermissionControlTooltip>
{showHeartbeatSettings() && (
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
<div className={cx('integration__actionItem')} onClick={() => setIsHearbeatFormOpen(true)}>
Heartbeat Settings
</div>
</WithPermissionControlTooltip>
)}
{!alertReceiveChannel.maintenance_till && (
<WithPermissionControlTooltip userAction={UserActions.MaintenanceWrite}>
@ -935,6 +932,12 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
</>
);
function showHeartbeatSettings() {
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
const heartbeat = heartbeatStore.items[heartbeatId];
return !!heartbeat?.last_heartbeat_time_verbal;
}
function deleteIntegration() {
alertReceiveChannelStore
.deleteAlertReceiveChannel(alertReceiveChannel.id)
@ -976,7 +979,7 @@ const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id
border={getVar('--border-weak')}
className={cx('how-to-connect__tag')}
>
<Text type="primary" size="small">
<Text type="primary" size="small" className={cx('radius')}>
HTTP Endpoint
</Text>
</Tag>
@ -1082,20 +1085,22 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
{renderHearbeat(alertReceiveChannel)}
<div className={cx('headerTop__item')}>
<Text type="secondary">Type:</Text>
<HorizontalGroup spacing="xs">
<IntegrationLogo scale={0.08} integration={integration} />
<Text type="primary">{integration?.display_name}</Text>
</HorizontalGroup>
</div>
<div className={cx('headerTop__item')}>
<Text type="secondary">Team:</Text>
<TeamName team={grafanaTeamStore.items[alertReceiveChannel.team]} />
</div>
<div className={cx('headerTop__item')}>
<Text type="secondary">Created by:</Text>
<UserDisplayWithAvatar id={alertReceiveChannel.author as any}></UserDisplayWithAvatar>
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px', marginLeft: '8px' }}>
<div className={cx('headerTop__item')}>
<Text type="secondary">Type:</Text>
<HorizontalGroup spacing="xs">
<IntegrationLogo scale={0.08} integration={integration} />
<Text type="primary">{integration?.display_name}</Text>
</HorizontalGroup>
</div>
<div className={cx('headerTop__item')}>
<Text type="secondary">Team:</Text>
<TeamName team={grafanaTeamStore.items[alertReceiveChannel.team]} />
</div>
<div className={cx('headerTop__item')}>
<Text type="secondary">Created by:</Text>
<UserDisplayWithAvatar id={alertReceiveChannel.author as any}></UserDisplayWithAvatar>
</div>
</div>
</div>
);

View file

@ -0,0 +1,129 @@
import { KeyValuePair } from 'utils';
export const TEXTAREA_ROWS_COUNT = 4;
export const MAX_CHARACTERS_COUNT = 50;
export const MONACO_OPTIONS = {
renderLineHighlight: false,
readOnly: true,
scrollbar: {
vertical: 'hidden',
horizontal: 'hidden',
verticalScrollbarSize: 0,
handleMouseWheel: false,
},
hideCursorInOverviewRuler: true,
minimap: { enabled: false },
cursorStyle: {
display: 'none',
},
};
export const MONACO_PAYLOAD_OPTIONS = {
renderLineHighlight: false,
readOnly: false,
hideCursorInOverviewRuler: true,
minimap: { enabled: false },
cursorStyle: {
display: 'none',
},
};
export const MONACO_INPUT_HEIGHT_SMALL = '32px';
export const MONACO_INPUT_HEIGHT_TALL = '120px';
export const BaseTemplateOptions = {
WebTitle: new KeyValuePair('web_title_template', 'Web Title'),
WebMessage: new KeyValuePair('web_message_template', 'Web Message'),
WebImage: new KeyValuePair('web_image_url_template', 'Web Image'),
Grouping: new KeyValuePair('grouping_id_template', 'Grouping'),
Resolve: new KeyValuePair('resolve_condition_template', 'Resolve condition'),
Routing: new KeyValuePair('route_template', 'Routing'),
SourceLink: new KeyValuePair('source_link_template', 'Source Link'),
Autoacknowledge: new KeyValuePair('acknowledge_condition_template', 'Autoacknowledge'),
Phone: new KeyValuePair('phone_call_title_template', 'Phone'),
SMS: new KeyValuePair('sms_title_template', 'SMS'),
SlackTitle: new KeyValuePair('slack_title_template', 'Title'),
SlackMessage: new KeyValuePair('slack_message_template', 'Message'),
SlackImage: new KeyValuePair('slack_image_url_template', 'Image'),
EmailTitle: new KeyValuePair('email_title_template', 'Title'),
EmailMessage: new KeyValuePair('email_message_template', 'Message'),
TelegramTitle: new KeyValuePair('telegram_title_template', 'Title'),
TelegramMessage: new KeyValuePair('telegram_message_template', 'Message'),
TelegramImage: new KeyValuePair('telegram_image_url_template', 'Image'),
Email: new KeyValuePair('Email', 'Email'),
Slack: new KeyValuePair('Slack', 'Slack'),
MSTeams: new KeyValuePair('Microsoft Teams', 'Microsoft Teams'),
Telegram: new KeyValuePair('Telegram', 'Telegram'),
};
export const BASE_INTEGRATION_TEMPLATES_LIST = [
{
label: BaseTemplateOptions.SourceLink.value,
value: BaseTemplateOptions.SourceLink.key,
},
{
label: BaseTemplateOptions.Autoacknowledge.value,
value: BaseTemplateOptions.Autoacknowledge.key,
},
{
label: BaseTemplateOptions.Phone.value,
value: BaseTemplateOptions.Phone.key,
},
{
label: BaseTemplateOptions.SMS.value,
value: BaseTemplateOptions.SMS.key,
},
{
label: BaseTemplateOptions.Email.value,
value: BaseTemplateOptions.Email.key,
children: [
{
label: BaseTemplateOptions.EmailTitle.value,
value: BaseTemplateOptions.EmailTitle.key,
},
{
label: BaseTemplateOptions.EmailMessage.value,
value: BaseTemplateOptions.EmailMessage.key,
},
],
},
{
label: BaseTemplateOptions.Slack.value,
value: BaseTemplateOptions.Slack.key,
children: [
{
label: BaseTemplateOptions.SlackTitle.value,
value: BaseTemplateOptions.SlackTitle.key,
},
{
label: BaseTemplateOptions.SlackMessage.value,
value: BaseTemplateOptions.SlackMessage.key,
},
{
label: BaseTemplateOptions.SlackImage.value,
value: BaseTemplateOptions.SlackImage.key,
},
],
},
{
label: BaseTemplateOptions.Telegram.value,
value: BaseTemplateOptions.Telegram.key,
children: [
{
label: BaseTemplateOptions.TelegramTitle.value,
value: BaseTemplateOptions.TelegramTitle.key,
},
{
label: BaseTemplateOptions.TelegramMessage.value,
value: BaseTemplateOptions.TelegramMessage.key,
},
{
label: BaseTemplateOptions.TelegramImage.value,
value: BaseTemplateOptions.TelegramImage.key,
},
],
},
];

View file

@ -44,4 +44,4 @@
&:hover {
background: var(--gray-9);
}
}
}

View file

@ -74,6 +74,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
const {
query: { p },
} = this.props;
this.setState({ page: p ? Number(p) : 1 }, this.update);
this.parseQueryParams();

View file

@ -68,3 +68,15 @@
margin-top: 8px;
opacity: 15%;
}
.u-flex-xs {
gap: 4px;
}
.u-margin-right-xs {
margin-right: 4px;
}
.u-margin-right-md {
margin-right: 8px;
}