Merge pull request #1990 from grafana/dev

v1.2.27
This commit is contained in:
Vadim Stepanov 2023-05-23 10:28:38 +01:00 committed by GitHub
commit ac9b82fb9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 842 additions and 784 deletions

View file

@ -5,6 +5,20 @@ 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.27 (2023-05-23)
### Added
- Allow passing Firebase credentials via environment variable by @vadimkerr ([#1969](https://github.com/grafana/oncall/pull/1969))
### Changed
- Update default Alertmanager templates by @iskhakov ([#1944](https://github.com/grafana/oncall/pull/1944))
### Fixed
- Fix SQLite permission issue by @vadimkerr ([#1984](https://github.com/grafana/oncall/pull/1984))
## v1.2.26 (2023-05-18)
### Fixed

View file

@ -19,9 +19,12 @@ RUN pip install -r requirements.txt
# https://stackoverflow.com/questions/34398632/docker-how-to-run-pip-requirements-txt-only-if-there-was-a-change/34399661#34399661
COPY ./ ./
# Collect static files and create an SQLite database
RUN mkdir -p /var/lib/oncall
# Collect static files
RUN DJANGO_SETTINGS_MODULE=settings.prod_without_db DATABASE_TYPE=sqlite3 DATABASE_NAME=/var/lib/oncall/oncall.db SECRET_KEY="ThEmUsTSecretKEYforBUILDstage123" SILK_PROFILER_ENABLED="True" python manage.py collectstatic --no-input
# Create SQLite database and set permissions
RUN mkdir -p /var/lib/oncall
RUN DATABASE_TYPE=sqlite3 DATABASE_NAME=/var/lib/oncall/oncall.db python manage.py create_sqlite_db
RUN chown -R 1000:2000 /var/lib/oncall
# This is required for silk profilers to sync between uwsgi workers

View file

@ -1,11 +1,8 @@
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.telegram.tasks import register_telegram_webhook
from apps.telegram.updates.update_manager import UpdateManager
register_telegram_webhook.delay()
class WebHookView(APIView):
def get(self, request, format=None):

View file

@ -20,10 +20,67 @@ Alerts from Grafana Alertmanager are automatically routed to this integration.
<br>Creating contact points and routes for other alertmanagers...
{% endif %}"""
# Default templates
# Web
web_title = """{{- payload.get("labels", {}).get("alertname", "No title (check Title Template)") -}}"""
web_message = """\
{%- set annotations = payload.annotations.copy() -%}
{%- set labels = payload.labels.copy() -%}
{%- if "summary" in annotations %}
{{ annotations.summary }}
{%- set _ = annotations.pop('summary') -%}
{%- endif %}
{%- if "message" in annotations %}
{{ annotations.message }}
{%- set _ = annotations.pop('message') -%}
{%- endif %}
{% set severity = labels.severity | default("Unknown") -%}
{%- set severity_emoji = {"critical": ":rotating_light:", "warning": ":warning:" }[severity] | default(":question:") -%}
Severity: {{ severity }} {{ severity_emoji }}
{%- set status = payload.status | default("Unknown") %}
{%- set status_emoji = {"firing": ":fire:", "resolved": ":white_check_mark:"}[status] | default(":warning:") %}
Status: {{ status }} {{ status_emoji }} (on the source)
{% if "runbook_url" in annotations -%}
[:book: Runbook:link:]({{ annotations.runbook_url }})
{%- set _ = annotations.pop('runbook_url') -%}
{%- endif %}
{%- if "runbook_url_internal" in annotations -%}
[:closed_book: Runbook (internal):link:]({{ annotations.runbook_url_internal }})
{%- set _ = annotations.pop('runbook_url_internal') -%}
{%- endif %}
:label: Labels:
{%- for k, v in payload["labels"].items() %}
- {{ k }}: {{ v }}
{%- endfor %}
{% if annotations | length > 0 -%}
:pushpin: Other annotations:
{%- for k, v in annotations.items() %}
- {{ k }}: {{ v }}
{%- endfor %}
{% endif %}
""" # noqa: W291
web_image_url = None
# Behaviour
source_link = "{{ payload.generatorURL }}"
grouping_id = "{{ payload.labels }}"
resolve_condition = """{{ payload.status == "resolved" }}"""
acknowledge_condition = None
# Slack
slack_title = """\
{# Usually title is located in payload.labels.alertname #}
{% set title = payload.get("labels", {}).get("alertname", "No title (check Web Title Template)") %}
{% set title = payload.get("labels", {}).get("alertname", "No title (check Title Template)") %}
{# Combine the title from different built-in variables into slack-formatted url #}
*<{{ grafana_oncall_link }}|#{{ grafana_oncall_incident_id }} {{ title }}>* via {{ integration_name }}
{% if source_link %}
@ -31,74 +88,131 @@ slack_title = """\
{%- endif %}
"""
slack_message = """\
{{- payload.message }}
{%- if "status" in payload -%}
*Status*: {{ payload.status }}
{% endif -%}
*Labels:* {% for k, v in payload["labels"].items() %}
{{ k }}: {{ v }}{% endfor %}
*Annotations:*
{%- for k, v in payload.get("annotations", {}).items() %}
{#- render annotation as slack markdown url if it starts with http #}
{{ k }}: {% if v.startswith("http") %} <{{v}}|here> {% else %} {{v}} {% endif -%}
{% endfor %}
""" # noqa: W291
# default slack message template is identical to web message template, except urls
# It can be based on web message template (see example), but it can affect existing templates
# slack_message = """
# {% set mkdwn_link_regex = "\[([\w\s\d:]+)\]\((https?:\/\/[\w\d./?=#]+)\)" %}
# {{ web_message
# | regex_replace(mkdwn_link_regex, "<\\2|\\1>")
# }}
# """
slack_message = """\
{%- set annotations = payload.annotations.copy() -%}
{%- set labels = payload.labels.copy() -%}
{%- if "summary" in annotations %}
{{ annotations.summary }}
{%- set _ = annotations.pop('summary') -%}
{%- endif %}
{%- if "message" in annotations %}
{{ annotations.message }}
{%- set _ = annotations.pop('message') -%}
{%- endif %}
{# Optionally set oncall_slack_user_group to slack user group in the following format "@users-oncall" #}
{%- set oncall_slack_user_group = None -%}
{%- if oncall_slack_user_group %}
Heads up {{ oncall_slack_user_group }}
{%- endif %}
{% set severity = labels.severity | default("Unknown") -%}
{%- set severity_emoji = {"critical": ":rotating_light:", "warning": ":warning:" }[severity] | default(":question:") -%}
Severity: {{ severity }} {{ severity_emoji }}
{%- set status = payload.status | default("Unknown") %}
{%- set status_emoji = {"firing": ":fire:", "resolved": ":white_check_mark:"}[status] | default(":warning:") %}
Status: {{ status }} {{ status_emoji }} (on the source)
{% if "runbook_url" in annotations -%}
<{{ annotations.runbook_url }}|:book: Runbook:link:>
{%- set _ = annotations.pop('runbook_url') -%}
{%- endif %}
{%- if "runbook_url_internal" in annotations -%}
<{{ annotations.runbook_url_internal }}|:closed_book: Runbook (internal):link:>
{%- set _ = annotations.pop('runbook_url_internal') -%}
{%- endif %}
:label: Labels:
{%- for k, v in payload["labels"].items() %}
- {{ k }}: {{ v }}
{%- endfor %}
{% if annotations | length > 0 -%}
:pushpin: Other annotations:
{%- for k, v in annotations.items() %}
- {{ k }}: {{ v }}
{%- endfor %}
{% endif %}
""" # noqa: W291
slack_image_url = None
web_title = """\
{# Usually title is located in payload.labels.alertname #}
{{- payload.get("labels", {}).get("alertname", "No title (check Web Title Template)") }}
"""
# SMS
sms_title = web_title
web_message = """\
{{- payload.message }}
{%- if "status" in payload %}
**Status**: {{ payload.status }}
{% endif -%}
**Labels:** {% for k, v in payload["labels"].items() %}
*{{ k }}*: {{ v }}{% endfor %}
**Annotations:**
{%- for k, v in payload.get("annotations", {}).items() %}
{#- render annotation as markdown url if it starts with http #}
*{{ k }}*: {% if v.startswith("http") %} [here]({{v}}){% else %} {{v}} {% endif -%}
{% endfor %}
# Phone
phone_call_title = web_title
# Telegram
telegram_title = web_title
# default telegram message template is identical to web message template, except urls
# It can be based on web message template (see example), but it can affect existing templates
# telegram_message = """
# {% set mkdwn_link_regex = "\[([\w\s\d:]+)\]\((https?:\/\/[\w\d./?=#]+)\)" %}
# {{ web_message
# | regex_replace(mkdwn_link_regex, "<a href='\\2'>\\1</a>")
# }}
# """
telegram_message = """\
{%- set annotations = payload.annotations.copy() -%}
{%- set labels = payload.labels.copy() -%}
{%- if "summary" in annotations %}
{{ annotations.summary }}
{%- set _ = annotations.pop('summary') -%}
{%- endif %}
{%- if "message" in annotations %}
{{ annotations.message }}
{%- set _ = annotations.pop('message') -%}
{%- endif %}
{% set severity = labels.severity | default("Unknown") -%}
{%- set severity_emoji = {"critical": ":rotating_light:", "warning": ":warning:" }[severity] | default(":question:") -%}
Severity: {{ severity }} {{ severity_emoji }}
{%- set status = payload.status | default("Unknown") %}
{%- set status_emoji = {"firing": ":fire:", "resolved": ":white_check_mark:"}[status] | default(":warning:") %}
Status: {{ status }} {{ status_emoji }} (on the source)
{% if "runbook_url" in annotations -%}
<a href='{{ annotations.runbook_url }}'>:book: Runbook:link:</a>
{%- set _ = annotations.pop('runbook_url') -%}
{%- endif %}
{%- if "runbook_url_internal" in annotations -%}
<a href='{{ annotations.runbook_url_internal }}'>:closed_book: Runbook (internal):link:</a>
{%- set _ = annotations.pop('runbook_url_internal') -%}
{%- endif %}
:label: Labels:
{%- for k, v in payload["labels"].items() %}
- {{ k }}: {{ v }}
{%- endfor %}
{% if annotations | length > 0 -%}
:pushpin: Other annotations:
{%- for k, v in annotations.items() %}
- {{ k }}: {{ v }}
{%- endfor %}
{% endif %}
""" # noqa: W291
web_image_url = slack_image_url
sms_title = '{{ payload.get("labels", {}).get("alertname", "Title undefined") }}'
phone_call_title = sms_title
telegram_title = sms_title
telegram_message = """\
{{- payload.messsage }}
{%- if "status" in payload -%}
<b>Status</b>: {{ payload.status }}
{% endif -%}
<b>Labels:</b> {% for k, v in payload["labels"].items() %}
{{ k }}: {{ v }}{% endfor %}
<b>Annotations:</b>
{%- for k, v in payload.get("annotations", {}).items() %}
{#- render annotation as markdown url if it starts with http #}
{{ k }}: {{ v }}
{% endfor %}""" # noqa: W291
telegram_image_url = slack_image_url
source_link = "{{ payload.generatorURL }}"
grouping_id = "{{ payload.labels }}"
resolve_condition = """\
{{ payload.get("status", "") == "resolved" }}
"""
acknowledge_condition = None
telegram_image_url = None
tests = {
"payload": {
@ -131,36 +245,12 @@ tests = {
"+-+kube_job_status_succeeded%7Bjob%3D%22kube-state-metrics%22%7D+%3E+0&g0.tab=1"
"|source>*)"
),
"message": (
"*Status*: firing\n"
"*Labels:* \n"
"job: kube-state-metrics\n"
"instance: 10.143.139.7:8443\n"
"job_name: email-tracking-perform-initialization-1.0.50\n"
"severity: warning\n"
"alertname: KubeJobCompletion\n"
"namespace: default\n"
"prometheus: monitoring/k8s\n"
"*Annotations:*\n"
"message: Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete. \n"
"runbook_url: <https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion|here> "
),
"message": "\nJob default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete.\n\n\n\nSeverity: warning :warning:\nStatus: firing :fire: (on the source)\n\n<https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion|:book: Runbook:link:>\n\n:label: Labels:\n- job: kube-state-metrics\n- instance: 10.143.139.7:8443\n- job_name: email-tracking-perform-initialization-1.0.50\n- severity: warning\n- alertname: KubeJobCompletion\n- namespace: default\n- prometheus: monitoring/k8s\n\n", # noqa
"image_url": None,
},
"web": {
"title": "KubeJobCompletion",
"message": """<p><strong>Status</strong>: firing <br/>
<strong>Labels:</strong> <br/>
<em>job</em>: kube-state-metrics <br/>
<em>instance</em>: 10.143.139.7:8443 <br/>
<em>job_name</em>: email-tracking-perform-initialization-1.0.50 <br/>
<em>severity</em>: warning <br/>
<em>alertname</em>: KubeJobCompletion <br/>
<em>namespace</em>: default <br/>
<em>prometheus</em>: monitoring/k8s <br/>
<strong>Annotations:</strong> <br/>
<em>message</em>: Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete. <br/>
<em>runbook_url</em>: <a href="https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion" rel="nofollow noopener" target="_blank">here</a></p>""", # noqa
"message": '<p>Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete. </p>\n<p>Severity: warning ⚠️ <br/>\nStatus: firing 🔥 (on the source) </p>\n<p><a href="https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion" rel="nofollow noopener" target="_blank">📖 Runbook🔗</a> </p>\n<p>🏷️ Labels: </p>\n<ul>\n<li>job: kube-state-metrics </li>\n<li>instance: 10.143.139.7:8443 </li>\n<li>job_name: email-tracking-perform-initialization-1.0.50 </li>\n<li>severity: warning </li>\n<li>alertname: KubeJobCompletion </li>\n<li>namespace: default </li>\n<li>prometheus: monitoring/k8s </li>\n</ul>', # noqa
"image_url": None,
},
"sms": {
@ -171,20 +261,7 @@ tests = {
},
"telegram": {
"title": "KubeJobCompletion",
"message": (
"<b>Status</b>: firing\n"
"<b>Labels:</b> \n"
"job: kube-state-metrics\n"
"instance: 10.143.139.7:8443\n"
"job_name: email-tracking-perform-initialization-1.0.50\n"
"severity: warning\n"
"alertname: KubeJobCompletion\n"
"namespace: default\n"
"prometheus: monitoring/k8s\n"
"<b>Annotations:</b>\n"
"message: Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete.\n\n"
"runbook_url: https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion\n"
),
"message": "\nJob default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete.\n\nSeverity: warning ⚠️\nStatus: firing 🔥 (on the source)\n\n<a href='https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion'>📖 Runbook🔗</a>\n\n🏷️ Labels:\n- job: kube-state-metrics\n- instance: 10.143.139.7:8443\n- job_name: email-tracking-perform-initialization-1.0.50\n- severity: warning\n- alertname: KubeJobCompletion\n- namespace: default\n- prometheus: monitoring/k8s\n\n", # noqa
"image_url": None,
},
}
@ -196,11 +273,12 @@ example_payload = {
"alerts": [
{
"status": "firing",
"labels": {
"alertname": "TestAlert",
"region": "eu-1",
"labels": {"alertname": "TestAlert", "region": "eu-1", "severity": "critical"},
"annotations": {
"message": "This is test alert",
"description": "This alert was sent by user for the demonstration purposes",
"runbook_url": "https://grafana.com/",
},
"annotations": {"description": "This alert was sent by user for the demonstration purposes"},
"startsAt": "2018-12-25T15:47:47.377363608Z",
"endsAt": "0001-01-01T00:00:00Z",
"generatorURL": "",

View file

@ -53,6 +53,13 @@ def on_after_setup_logger(logger, **kwargs):
)
@celery.signals.worker_ready.connect
def on_worker_ready(*args, **kwargs):
from apps.telegram.tasks import register_telegram_webhook
register_telegram_webhook.delay()
if settings.OTEL_TRACING_ENABLED and settings.OTEL_EXPORTER_OTLP_ENDPOINT:
@celery.signals.worker_process_init.connect(weak=False)

View file

@ -0,0 +1,15 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import connection
class Command(BaseCommand):
"""
Create SQLite3 database file if it doesn't exist.
"""
def handle(self, *args, **options):
assert settings.DATABASE_TYPE == "sqlite3"
# Creating a cursor creates the database file if it doesn't exist.
connection.cursor()

View file

@ -1,8 +1,10 @@
import base64
import json
import os
from random import randrange
from celery.schedules import crontab
from firebase_admin import initialize_app
from firebase_admin import credentials, initialize_app
from common.utils import getenv_boolean, getenv_integer
@ -587,13 +589,21 @@ EXTRA_MESSAGING_BACKENDS = [
("apps.mobile_app.backend.MobileAppCriticalBackend", 6),
]
FIREBASE_APP = initialize_app(options={"projectId": os.environ.get("FCM_PROJECT_ID", None)})
# Firebase credentials can be passed as base64 encoded JSON string in GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64 env variable.
# If it's not passed, firebase_admin will use a file located at GOOGLE_APPLICATION_CREDENTIALS env variable.
credential = None
GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64 = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64", None)
if GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64:
credentials_json = json.loads(base64.b64decode(GOOGLE_APPLICATION_CREDENTIALS_JSON_BASE64))
credential = credentials.Certificate(credentials_json)
# FCM_PROJECT_ID can be different from the project ID in the credentials file.
FCM_PROJECT_ID = os.environ.get("FCM_PROJECT_ID", None)
FCM_RELAY_ENABLED = getenv_boolean("FCM_RELAY_ENABLED", default=False)
FCM_DJANGO_SETTINGS = {
# an instance of firebase_admin.App to be used as default for all fcm-django requests
# default: None (the default Firebase app)
"DEFAULT_FIREBASE_APP": None,
"DEFAULT_FIREBASE_APP": initialize_app(credential=credential, options={"projectId": FCM_PROJECT_ID}),
"APP_VERBOSE_NAME": "OnCall",
"ONE_DEVICE_PER_USER": True,
"DELETE_INACTIVE_DEVICES": False,

View file

@ -5,9 +5,9 @@
&:before {
content: '';
position: absolute;
height: calc(100% - 10px);
height: calc(100% - 20px);
border: var(--border-weak);
margin-top: 0px;
margin-top: 20px;
margin-left: -20px;
}
}
@ -45,4 +45,4 @@
display: flex;
align-items: center;
justify-content: center;
}
}

View file

@ -4,6 +4,7 @@
margin-bottom: 12px;
&__content {
width: 100%;
padding-top: 12px;
padding-bottom: 12px;
}

View file

@ -0,0 +1,18 @@
.container {
display: flex;
flex-direction: row;
gap: 4px;
&__item {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
display: flex;
flex-direction: row;
gap: 4px;
}
label {
margin-right: 0px;
}
}

View file

@ -0,0 +1,55 @@
import React from 'react';
import { Button, InlineLabel, LoadingPlaceholder, Tooltip } from '@grafana/ui';
import cn from 'classnames/bind';
import styles from './IntegrationTemplateBlock.module.scss';
const cx = cn.bind(styles);
interface IntegrationTemplateBlockProps {
label: string;
labelTooltip?: string;
renderInput: () => React.ReactNode;
showHelp?: boolean;
isLoading?: boolean;
onEdit: (templateName) => void;
onRemove?: () => void;
onHelp?: () => void;
}
const IntegrationTemplateBlock: React.FC<IntegrationTemplateBlockProps> = ({
label,
labelTooltip,
renderInput,
onEdit,
onRemove,
isLoading,
}) => {
let inlineLabelProps = { labelTooltip };
if (!labelTooltip) {
delete inlineLabelProps.labelTooltip;
}
return (
<div className={cx('container')}>
<InlineLabel width={20} {...inlineLabelProps}>
{label}
</InlineLabel>
<div className={cx('container__item')}>
{renderInput()}
<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>
{isLoading && <LoadingPlaceholder text="Loading..." />}
</div>
</div>
);
};
export default IntegrationTemplateBlock;

View file

@ -3,4 +3,5 @@
line-height: 100%;
padding: 5px 8px;
color: white;
white-space: nowrap;
}

View file

@ -0,0 +1,34 @@
.spacing {
margin-bottom: 12px;
}
.icon-exclamation {
color: var(--error-text-color);
}
.heading-container {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: hidden;
gap: 12px;
&__item {
display: flex;
white-space: nowrap;
flex-direction: row;
gap: 8px;
}
&__item--large {
flex-grow: 1;
overflow: hidden;
}
&__text {
overflow: hidden;
max-width: calc(100% - 48px);
text-overflow: ellipsis;
}
}

View file

@ -1,21 +1,20 @@
import React, { useState } from 'react';
import { ConfirmModal, HorizontalGroup, Icon, IconButton } from '@grafana/ui';
import { ConfirmModal, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import IntegrationBlock from 'components/Integrations/IntegrationBlock';
import PluginLink from 'components/PluginLink/PluginLink';
import Text from 'components/Text/Text';
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
import styles from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss';
import { RouteButtonsDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { ChannelFilter } from 'models/channel_filter';
import IntegrationHelper from 'pages/integration_2/Integration2.helper';
import { useStore } from 'state/useStore';
import styles from './CollapsedIntegrationRouteDisplay.module.scss';
import { RouteButtonsDisplay } from './ExpandedIntegrationRouteDisplay';
import IntegrationHelper from './Integration2.helper';
import IntegrationBlock from './IntegrationBlock';
const cx = cn.bind(styles);
interface CollapsedIntegrationRouteDisplayProps {
@ -42,8 +41,8 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
hasCollapsedBorder={false}
key={channelFilterId}
heading={
<HorizontalGroup justify={'space-between'}>
<HorizontalGroup spacing={'md'}>
<div className={cx('heading-container')}>
<div className={cx('heading-container__item', 'heading-container__item--large')}>
<TooltipBadge
borderType="success"
text={IntegrationHelper.getRouteConditionWording(
@ -54,34 +53,39 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
tooltipContent={undefined}
/>
{channelFilter.filtering_term && (
<Text type="primary">{IntegrationHelper.truncateLine(channelFilter.filtering_term)}</Text>
<Text type="primary" className={cx('heading-container__text')}>
{channelFilter.filtering_term}
</Text>
)}
</HorizontalGroup>
<HorizontalGroup>
</div>
<div className={cx('heading-container__item')}>
<RouteButtonsDisplay
alertReceiveChannelId={alertReceiveChannelId}
channelFilterId={channelFilterId}
routeIndex={routeIndex}
setRouteIdForDeletion={() => setRouteIdForDeletion(channelFilterId)}
/>
</HorizontalGroup>
</HorizontalGroup>
</div>
</div>
}
content={
<div className={cx('spacing')}>
<HorizontalGroup>
{channelFilter.slack_channel?.display_name && (
<HorizontalGroup>
<VerticalGroup>
{IntegrationHelper.getChatOpsChannels(channelFilter).map((chatOpsChannel, key) => (
<HorizontalGroup key={key}>
<Text type="secondary">Publish to ChatOps</Text>
<Icon name="slack" />
<Text type="primary" strong>
{channelFilter.slack_channel.display_name}
{chatOpsChannel}
</Text>
</HorizontalGroup>
)}
))}
<HorizontalGroup>
<Icon name="list-ui-alt" />
<Text type="secondary">Escalate to</Text>
{escalationChain?.name && (
<PluginLink
className={cx('hover-button')}
@ -93,15 +97,19 @@ const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDispla
</Text>
</PluginLink>
)}
{!escalationChain?.name && (
<IconButton
name="info-circle"
tooltip={'You have no selected escalation chain for this route'}
size={'md'}
/>
<HorizontalGroup spacing={'xs'}>
<div className={cx('icon-exclamation')}>
<Icon name="exclamation-triangle" />
</div>
<Text type="primary" strong>
No Escalation chain
</Text>
</HorizontalGroup>
)}
</HorizontalGroup>
</HorizontalGroup>
</VerticalGroup>
</div>
}
/>

View file

@ -5,6 +5,8 @@ import { Button, HorizontalGroup, InlineLabel, VerticalGroup, Icon, Tooltip, Con
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import IntegrationBlock from 'components/Integrations/IntegrationBlock';
import IntegrationBlockItem from 'components/Integrations/IntegrationBlockItem';
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
import PluginLink from 'components/PluginLink/PluginLink';
import Text from 'components/Text/Text';
@ -12,20 +14,18 @@ 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 styles from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss';
import TeamName from 'containers/TeamName/TeamName';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
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 { MONACO_INPUT_HEIGHT_SMALL, MONACO_OPTIONS } from 'pages/integration_2/Integration2.config';
import IntegrationHelper from 'pages/integration_2/Integration2.helper';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
import { UserActions } from 'utils/authorization';
import styles from './ExpandedIntegrationRouteDisplay.module.scss';
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_OPTIONS } from './Integration2.config';
import IntegrationHelper from './Integration2.helper';
import IntegrationBlock from './IntegrationBlock';
import IntegrationBlockItem from './IntegrationBlockItem';
const cx = cn.bind(styles);
interface ExpandedIntegrationRouteDisplayProps {
@ -45,8 +45,19 @@ interface ExpandedIntegrationRouteDisplayState {
const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayProps> = observer(
({ alertReceiveChannelId, channelFilterId, templates, routeIndex, openEditTemplateModal, onEditRegexpTemplate }) => {
const { escalationPolicyStore, escalationChainStore, alertReceiveChannelStore, grafanaTeamStore } = useStore();
const hasChatOpsConnectors = false;
const store = useStore();
const {
telegramChannelStore,
teamStore,
escalationPolicyStore,
escalationChainStore,
alertReceiveChannelStore,
grafanaTeamStore,
} = store;
const isSlackInstalled = Boolean(teamStore.currentTeam?.slack_team_identity);
const isTelegramInstalled =
store.hasFeature(AppFeature.Telegram) && telegramChannelStore.currentTeamToTelegramChannel?.length > 0;
const [{ isEscalationCollapsed, isRefreshingEscalationChains, routeIdForDeletion }, setState] = useReducer(
(state: ExpandedIntegrationRouteDisplayState, newState: Partial<ExpandedIntegrationRouteDisplayState>) => ({
@ -71,6 +82,9 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
escalationChainRedirectObj.id = channelFilter.escalation_chain;
}
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId];
const isDefault = IntegrationHelper.getRouteConditionWording(channelFilterIds, routeIndex) === 'Default';
return (
<>
<IntegrationBlock
@ -81,10 +95,7 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
<HorizontalGroup spacing={'md'}>
<TooltipBadge
borderType="success"
text={IntegrationHelper.getRouteConditionWording(
alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId],
routeIndex
)}
text={IntegrationHelper.getRouteConditionWording(channelFilterIds, routeIndex)}
tooltipTitle={undefined}
tooltipContent={undefined}
/>
@ -101,31 +112,30 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
}
content={
<VerticalGroup spacing="xs">
<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}
{/* Show Routing Template only for If/Else Routes, not for Default */}
{!isDefault && (
<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}
/>
</div>
<Button
variant={'secondary'}
icon="edit"
size={'md'}
onClick={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
/>
</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>
</HorizontalGroup>
</IntegrationBlockItem>
)}
{routeIndex !== channelFiltersTotal.length - 1 && (
<IntegrationBlockItem>
@ -138,7 +148,7 @@ const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayP
</IntegrationBlockItem>
)}
{hasChatOpsConnectors && (
{(isSlackInstalled || isTelegramInstalled) && (
<IntegrationBlockItem>
<VerticalGroup spacing="md">
<Text type="primary">Publish to ChatOps</Text>

View file

@ -0,0 +1,131 @@
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_INPUT_HEIGHT_TALL } from 'pages/integration_2/Integration2.config';
interface TemplateToRender {
name: string;
label: string;
height: string;
}
interface TemplateBlock {
name: string;
contents: TemplateToRender[];
}
export const templatesToRender: TemplateBlock[] = [
{
name: null,
contents: [
{
name: 'grouping_id_template',
label: 'Grouping',
height: MONACO_INPUT_HEIGHT_TALL,
},
{
name: 'resolve_condition_template',
label: 'Auto resolve',
height: MONACO_INPUT_HEIGHT_SMALL,
},
],
},
{
name: 'Web',
contents: [
{
name: 'web_title_template',
label: 'Title',
height: MONACO_INPUT_HEIGHT_TALL,
},
{
name: 'web_message_template',
label: 'Message',
height: MONACO_INPUT_HEIGHT_TALL,
},
{
name: 'web_image_url_template',
label: 'Image',
height: MONACO_INPUT_HEIGHT_SMALL,
},
],
},
{
name: null,
contents: [
{
name: 'acknowledge_condition_template',
label: 'Auto acknowledge',
height: MONACO_INPUT_HEIGHT_SMALL,
},
{
name: 'source_link_template',
label: 'Source link',
height: MONACO_INPUT_HEIGHT_SMALL,
},
],
},
{
name: null,
contents: [
{
name: 'phone_call_title_template',
label: 'Phone Call',
height: MONACO_INPUT_HEIGHT_SMALL,
},
{
name: 'sms_title_template',
label: 'SMS',
height: MONACO_INPUT_HEIGHT_SMALL,
},
],
},
{
name: 'Slack',
contents: [
{
name: 'slack_title_template',
label: 'Title',
height: MONACO_INPUT_HEIGHT_SMALL,
},
{
name: 'slack_message_template',
label: 'Message',
height: MONACO_INPUT_HEIGHT_TALL,
},
{
name: 'slack_image_url_template',
label: 'Image',
height: MONACO_INPUT_HEIGHT_SMALL,
},
],
},
{
name: 'Telegram',
contents: [
{
name: 'telegram_title_template',
label: 'Title',
height: MONACO_INPUT_HEIGHT_SMALL,
},
{
name: 'telegram_message_template',
label: 'Message',
height: MONACO_INPUT_HEIGHT_TALL,
},
{
name: 'telegram_image_url_template',
label: 'Image',
height: MONACO_INPUT_HEIGHT_SMALL,
},
],
},
{
name: 'Email',
contents: [
{
name: 'email_title_template',
label: 'Title',
height: MONACO_INPUT_HEIGHT_SMALL,
},
{ name: 'email_message_template', label: 'Message', height: MONACO_INPUT_HEIGHT_TALL },
],
},
];

View file

@ -0,0 +1,131 @@
import React, { useState } from 'react';
import { ConfirmModal } from '@grafana/ui';
import cn from 'classnames/bind';
import IntegrationBlockItem from 'components/Integrations/IntegrationBlockItem';
import IntegrationTemplateBlock from 'components/Integrations/IntegrationTemplateBlock';
import MonacoEditor from 'components/MonacoEditor/MonacoEditor';
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 { useStore } from 'state/useStore';
import { openErrorNotification, openNotification } from 'utils';
const cx = cn.bind(styles);
interface IntegrationTemplateListProps {
templates: AlertTemplatesDTO[];
alertReceiveChannelId: AlertReceiveChannel['id'];
openEditTemplateModal: (templateName: string | string[]) => void;
}
const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
templates,
openEditTemplateModal,
alertReceiveChannelId,
}) => {
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
</Text>
</IntegrationBlockItem>
{templatesToRender.map((template, key) => (
<IntegrationBlockItem key={key}>
<VerticalBlock>
{template.name && <Text type={'primary'}>{template.name}</Text>}
{template.contents.map((contents, innerKey) => (
<IntegrationTemplateBlock
key={innerKey}
isLoading={isRestoringTemplate && templateRestoreName === contents.name}
onRemove={() => onShowConfirmModal(contents.name)}
label={contents.label}
renderInput={() => (
<div className={cx('input')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(
templates[contents.name] || '',
contents.height === MONACO_INPUT_HEIGHT_TALL
)}
disabled={true}
height={contents.height}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal(contents.name)}
/>
))}
</VerticalBlock>
</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);
});
}
};
const VerticalBlock: React.FC<{ children: any[] }> = ({ children }) => {
return <div className={cx('vertical-block')}>{children}</div>;
};
export default IntegrationTemplateList;

View file

@ -1,3 +0,0 @@
.spacing {
margin-bottom: 12px;
}

View file

@ -1,7 +1,12 @@
/*
[oncall-private]
Any change to this file needs to be done in the oncall-private also
*/
import dayjs from 'dayjs';
import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
import { ChannelFilter } from 'models/channel_filter';
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { MAX_CHARACTERS_COUNT, TEXTAREA_ROWS_COUNT } from './Integration2.config';
@ -31,7 +36,7 @@ const IntegrationHelper = {
return slice.length === line.length ? slice : `${slice} ...`;
},
getRouteConditionWording(channelFilters: Array<ChannelFilter['id']>, routeIndex: number) {
getRouteConditionWording(channelFilters: Array<ChannelFilter['id']>, routeIndex: number): 'Default' | 'Else' | 'If' {
const totalCount = Object.keys(channelFilters).length;
if (routeIndex === totalCount - 1) {
@ -54,6 +59,19 @@ const IntegrationHelper = {
return `${hourDiff}h left`;
},
getChatOpsChannels(channelFilter: ChannelFilter) {
const channels = [];
if (channelFilter.notify_in_slack && channelFilter.slack_channel?.display_name) {
channels.push(channelFilter.slack_channel.display_name);
}
if (channelFilter.telegram_channel) {
channels.push(channelFilter.telegram_channel);
}
return channels;
},
};
export default IntegrationHelper;

View file

@ -124,12 +124,7 @@ $LARGE-MARGIN: 24px;
}
.input {
&--short {
width: 500px;
}
&--long {
width: 700px;
}
flex-grow: 1;
}
.how-to-connect__container {
@ -146,15 +141,50 @@ $LARGE-MARGIN: 24px;
padding: 4px 8px;
}
.templates__content {
padding-left: 12px;
.vertical-block {
display: flex;
flex-direction: column;
gap: 8px;
}
.templates {
&__content {
padding-left: 12px;
}
&__container {
width: 100%;
padding-right: 48px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: hidden;
gap: 12px;
}
&__item {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&__item--large {
max-width: calc((100% - 150px) / 2);
}
&__item--small {
max-width: 150px;
}
&__item-text {
margin-right: 4px;
}
}
.templates__content,
.templates__container {
.templates__outer-container {
display: flex;
width: 100%;
align-items: center;
overflow: hidden;
}
.templates__edit {
@ -164,3 +194,7 @@ $LARGE-MARGIN: 24px;
.template-drawer {
padding-bottom: 24px;
}
.no-wrap {
white-space: nowrap;
}

View file

@ -27,6 +27,7 @@ import IntegrationCollapsibleTreeView, {
} from 'components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView';
import IntegrationInputField from 'components/IntegrationInputField/IntegrationInputField';
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
import IntegrationBlock from 'components/Integrations/IntegrationBlock';
import MonacoEditor, { MONACO_LANGUAGE } from 'components/MonacoEditor/MonacoEditor';
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
import { initErrorDataState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
@ -36,6 +37,10 @@ import Text from 'components/Text/Text';
import TooltipBadge from 'components/TooltipBadge/TooltipBadge';
import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
import EditRegexpRouteTemplateModal from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal';
import CollapsedIntegrationRouteDisplay from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay';
import ExpandedIntegrationRouteDisplay from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay';
import Integration2HeartbeatForm from 'containers/IntegrationContainers/Integration2HearbeatForm/Integration2HeartbeatForm';
import IntegrationTemplateList from 'containers/IntegrationContainers/IntegrationTemplatesList';
import IntegrationForm2 from 'containers/IntegrationForm/IntegrationForm2';
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
import MaintenanceForm from 'containers/MaintenanceForm/MaintenanceForm';
@ -47,6 +52,9 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_
import { ChannelFilter } from 'models/channel_filter';
import { MaintenanceType } from 'models/maintenance/maintenance.types';
import { API_HOST, API_PATH_PREFIX } from 'network';
import { INTEGRATION_TEMPLATES_LIST, MONACO_PAYLOAD_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 { PageProps, WithStoreProps } from 'state/types';
import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore';
@ -56,15 +64,6 @@ import LocationHelper from 'utils/LocationHelper';
import { UserActions } from 'utils/authorization';
import { DATASOURCE_ALERTING, PLUGIN_ROOT } from 'utils/consts';
import CollapsedIntegrationRouteDisplay from './CollapsedIntegrationRouteDisplay';
import ExpandedIntegrationRouteDisplay from './ExpandedIntegrationRouteDisplay';
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';
const cx = cn.bind(styles);
interface Integration2Props extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
@ -158,7 +157,7 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
<div className={cx('root')}>
{isTemplateSettingsOpen && (
<Drawer
width="75%"
width="640px"
scrollableContent
title="Template Settings"
onClose={() => this.setState({ isTemplateSettingsOpen: false })}
@ -197,63 +196,67 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
</Text>
)}
<HorizontalGroup>
{alertReceiveChannelCounter && (
<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>
)}
<div className={cx('no-wrap')}>
<HorizontalGroup>
{alertReceiveChannelCounter && (
<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
borderType="success"
icon="link"
text={channelFilterIds.length}
tooltipTitle={`${channelFilterIds.length} Routes`}
tooltipContent={undefined}
/>
{alertReceiveChannel.maintenance_till && (
<TooltipBadge
borderType="primary"
icon="pause"
text={IntegrationHelper.getMaintenanceText(alertReceiveChannel.maintenance_till)}
tooltipTitle={IntegrationHelper.getMaintenanceText(
alertReceiveChannel.maintenance_till,
alertReceiveChannel.maintenance_mode
)}
borderType="success"
icon="link"
text={channelFilterIds.length}
tooltipTitle={`${channelFilterIds.length} Routes`}
tooltipContent={undefined}
/>
)}
{this.renderHearbeat(alertReceiveChannel)}
{alertReceiveChannel.maintenance_till && (
<TooltipBadge
borderType="primary"
icon="pause"
text={IntegrationHelper.getMaintenanceText(alertReceiveChannel.maintenance_till)}
tooltipTitle={IntegrationHelper.getMaintenanceText(
alertReceiveChannel.maintenance_till,
alertReceiveChannel.maintenance_mode
)}
tooltipContent={undefined}
/>
)}
{this.renderHearbeat(alertReceiveChannel)}
<HorizontalGroup spacing="xs">
<Text type="secondary">Type:</Text>
<HorizontalGroup spacing="xs">
<IntegrationLogo scale={0.08} integration={integration} />
<Text type="primary">{integration?.display_name}</Text>
<Text type="secondary">Type:</Text>
<HorizontalGroup spacing="xs">
<IntegrationLogo scale={0.08} integration={integration} />
<Text type="primary">{integration?.display_name}</Text>
</HorizontalGroup>
</HorizontalGroup>
<HorizontalGroup spacing="xs">
<Text type="secondary">Team:</Text>
<TeamName team={grafanaTeamStore.items[alertReceiveChannel.team]} size="small" />
</HorizontalGroup>
<HorizontalGroup spacing="xs">
<Text type="secondary">Created by:</Text>
<UserDisplayWithAvatar id={alertReceiveChannel.author as any}></UserDisplayWithAvatar>
</HorizontalGroup>
</HorizontalGroup>
<HorizontalGroup spacing="xs">
<Text type="secondary">Team:</Text>
<TeamName team={grafanaTeamStore.items[alertReceiveChannel.team]} size="small" />
</HorizontalGroup>
<HorizontalGroup spacing="xs">
<Text type="secondary">Created by:</Text>
<UserDisplayWithAvatar id={alertReceiveChannel.author as any}></UserDisplayWithAvatar>
</HorizontalGroup>
</HorizontalGroup>
</div>
</div>
<IntegrationCollapsibleTreeView
@ -273,7 +276,7 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
<IntegrationBlock
hasCollapsedBorder
heading={
<div className={cx('templates__container')}>
<div className={cx('templates__outer-container')}>
<Tag
color={getVar('--tag-secondary-transparent')}
border={getVar('--border-weak')}
@ -285,26 +288,32 @@ class Integration2 extends React.Component<Integration2Props, Integration2State>
</Tag>
<div className={cx('templates__content')}>
<HorizontalGroup>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Grouping:</Text>
<div className={cx('templates__container')}>
<div className={cx('templates__item', 'templates__item--large')}>
<Text type="secondary" className={cx('templates__item-text')}>
Grouping:
</Text>
<Text type="primary">
{IntegrationHelper.truncateLine(templates['grouping_id_template'] || '')}
</Text>
</HorizontalGroup>
</div>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Autoresolve:</Text>
<div className={cx('templates__item', 'templates__item--large')}>
<Text type="secondary" className={cx('templates__item-text')}>
Autoresolve:
</Text>
<Text type="primary">
{IntegrationHelper.truncateLine(templates['resolve_condition_template'] || '')}
</Text>
</HorizontalGroup>
</div>
<HorizontalGroup spacing={'xs'}>
<Text type="secondary">Visualisation:</Text>
<div className={cx('templates__item', 'templates__item--small')}>
<Text type="secondary" className={cx('templates__item-text')}>
Visualisation:
</Text>
<Text type="primary">Multiple</Text>
</HorizontalGroup>
</HorizontalGroup>
</div>
</div>
<div className={cx('templates__edit')}>
<Button
@ -855,7 +864,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({ alertReceiveCha
</div>
<div className={cx('integration__actionItem')} onClick={() => setIsHearbeatFormOpen(true)}>
Hearbeat
Hearbeat Settings
</div>
{!alertReceiveChannel.maintenance_till && (
@ -899,13 +908,13 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({ alertReceiveCha
onClick={() => {
setConfirmModal({
isOpen: true,
title: (
<>
title: 'Delete Integration?',
body: (
<Text type="primary">
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} />{' '}
integration?
</>
integration?{' '}
</Text>
),
body: <>This action cannot be undone.</>,
onConfirm: deleteIntegration,
dismissText: 'Cancel',
confirmText: 'Delete',

View file

@ -1,59 +0,0 @@
import React from 'react';
import { Button, HorizontalGroup, Icon, InlineLabel, LoadingPlaceholder, Tooltip } from '@grafana/ui';
import Text from 'components/Text/Text';
interface IntegrationTemplateBlockProps {
label: string;
labelTooltip?: string;
renderInput: () => React.ReactNode;
showHelp?: boolean;
isLoading?: boolean;
onEdit: (templateName) => void;
onRemove?: () => void;
onHelp?: () => void;
}
const IntegrationTemplateBlock: React.FC<IntegrationTemplateBlockProps> = ({
label,
labelTooltip,
renderInput,
showHelp,
onEdit,
onHelp,
onRemove,
isLoading,
}) => {
let inlineLabelProps = { labelTooltip };
if (!labelTooltip) {
delete inlineLabelProps.labelTooltip;
}
return (
<HorizontalGroup align={'flex-start'} spacing={'xs'}>
<InlineLabel width={20} {...inlineLabelProps}>
{label}
</InlineLabel>
{renderInput()}
<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>
);
};
export default IntegrationTemplateBlock;

View file

@ -1,454 +0,0 @@
import React, { useState } from 'react';
import { ConfirmModal, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
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';
import styles from './Integration2.module.scss';
import IntegrationBlockItem from './IntegrationBlockItem';
import IntegrationTemplateBlock from './IntegrationTemplateBlock';
const cx = cn.bind(styles);
interface IntegrationTemplateListProps {
templates: AlertTemplatesDTO[];
alertReceiveChannelId: AlertReceiveChannel['id'];
openEditTemplateModal: (templateName: string | string[]) => void;
}
const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
templates,
openEditTemplateModal,
alertReceiveChannelId,
}) => {
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
</Text>
</IntegrationBlockItem>
<IntegrationBlockItem>
<VerticalGroup>
<IntegrationTemplateBlock
onRemove={() => onShowConfirmModal('grouping_id_template')}
isLoading={isRestoringTemplate && templateRestoreName === 'grouping_id_template'}
label={'Grouping'}
renderInput={() => (
<div className={cx('input', 'input--short')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['grouping_id_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
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')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['resolve_condition_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('resolve_condition_template')}
/>
</VerticalGroup>
</IntegrationBlockItem>
<IntegrationBlockItem>
<VerticalGroup>
<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')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['web_title_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('web_title_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'web_message_template'}
onRemove={() => onShowConfirmModal('web_message_template')}
label={'Message'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['web_message_template'] || '', true)}
disabled={true}
height={MONACO_INPUT_HEIGHT_TALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('web_message_template')}
/>
<IntegrationTemplateBlock
isLoading={isRestoringTemplate && templateRestoreName === 'web_image_url_template'}
onRemove={() => onShowConfirmModal('web_image_url_template')}
label={'Image'}
renderInput={() => (
<div className={cx('input', 'input--long')}>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(templates['web_image_url_template'] || '', false)}
disabled={true}
height={MONACO_INPUT_HEIGHT_SMALL}
data={templates}
showLineNumbers={false}
monacoOptions={MONACO_OPTIONS}
/>
</div>
)}
onEdit={() => openEditTemplateModal('web_image_url_template')}
/>
</VerticalGroup>
</IntegrationBlockItem>
<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')}
/>
<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>
)}
onEdit={() => openEditTemplateModal('source_link_template')}
/>
</VerticalGroup>
</IntegrationBlockItem>
<IntegrationBlockItem>
<VerticalGroup>
<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')}
/>
<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;