diff --git a/CHANGELOG.md b/CHANGELOG.md index 64db26d8..78bfcd28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/engine/Dockerfile b/engine/Dockerfile index bf5672a5..ad1bbd6c 100644 --- a/engine/Dockerfile +++ b/engine/Dockerfile @@ -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 diff --git a/engine/apps/telegram/views.py b/engine/apps/telegram/views.py index 76740711..6db87e99 100644 --- a/engine/apps/telegram/views.py +++ b/engine/apps/telegram/views.py @@ -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): diff --git a/engine/config_integrations/alertmanager.py b/engine/config_integrations/alertmanager.py index 8a2f8464..579174de 100644 --- a/engine/config_integrations/alertmanager.py +++ b/engine/config_integrations/alertmanager.py @@ -20,10 +20,67 @@ Alerts from Grafana Alertmanager are automatically routed to this integration.
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, "\\1") +# }} +# """ +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 -%} +:book: Runbook:link: +{%- set _ = annotations.pop('runbook_url') -%} +{%- endif %} + +{%- if "runbook_url_internal" in annotations -%} +: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 - -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 -%} -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 }}: {{ 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: " - ), + "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\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": """

Status: firing
-Labels:
-job: kube-state-metrics
-instance: 10.143.139.7:8443
-job_name: email-tracking-perform-initialization-1.0.50
-severity: warning
-alertname: KubeJobCompletion
-namespace: default
-prometheus: monitoring/k8s
-Annotations:
-message: Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete.
-runbook_url: here

""", # noqa + "message": '

Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete.

\n

Severity: warning ⚠️
\nStatus: firing 🔥 (on the source)

\n

📖 Runbook🔗

\n

🏷️ Labels:

\n', # noqa "image_url": None, }, "sms": { @@ -171,20 +261,7 @@ tests = { }, "telegram": { "title": "KubeJobCompletion", - "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\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📖 Runbook🔗\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": "", diff --git a/engine/engine/celery.py b/engine/engine/celery.py index c78459d5..c7ac45fa 100644 --- a/engine/engine/celery.py +++ b/engine/engine/celery.py @@ -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) diff --git a/engine/engine/management/commands/create_sqlite_db.py b/engine/engine/management/commands/create_sqlite_db.py new file mode 100644 index 00000000..eb99b439 --- /dev/null +++ b/engine/engine/management/commands/create_sqlite_db.py @@ -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() diff --git a/engine/settings/base.py b/engine/settings/base.py index eb9ed5d9..12c7ee66 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -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, diff --git a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.module.scss b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.module.scss index bd73eb3c..e5d8e9d4 100644 --- a/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.module.scss +++ b/grafana-plugin/src/components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView.module.scss @@ -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; -} \ No newline at end of file +} diff --git a/grafana-plugin/src/pages/integration_2/IntegrationBlock.module.scss b/grafana-plugin/src/components/Integrations/IntegrationBlock.module.scss similarity index 100% rename from grafana-plugin/src/pages/integration_2/IntegrationBlock.module.scss rename to grafana-plugin/src/components/Integrations/IntegrationBlock.module.scss diff --git a/grafana-plugin/src/pages/integration_2/IntegrationBlock.tsx b/grafana-plugin/src/components/Integrations/IntegrationBlock.tsx similarity index 100% rename from grafana-plugin/src/pages/integration_2/IntegrationBlock.tsx rename to grafana-plugin/src/components/Integrations/IntegrationBlock.tsx diff --git a/grafana-plugin/src/pages/integration_2/IntegrationBlockItem.module.scss b/grafana-plugin/src/components/Integrations/IntegrationBlockItem.module.scss similarity index 93% rename from grafana-plugin/src/pages/integration_2/IntegrationBlockItem.module.scss rename to grafana-plugin/src/components/Integrations/IntegrationBlockItem.module.scss index bad75ae3..f183c3ff 100644 --- a/grafana-plugin/src/pages/integration_2/IntegrationBlockItem.module.scss +++ b/grafana-plugin/src/components/Integrations/IntegrationBlockItem.module.scss @@ -4,6 +4,7 @@ margin-bottom: 12px; &__content { + width: 100%; padding-top: 12px; padding-bottom: 12px; } diff --git a/grafana-plugin/src/pages/integration_2/IntegrationBlockItem.tsx b/grafana-plugin/src/components/Integrations/IntegrationBlockItem.tsx similarity index 100% rename from grafana-plugin/src/pages/integration_2/IntegrationBlockItem.tsx rename to grafana-plugin/src/components/Integrations/IntegrationBlockItem.tsx diff --git a/grafana-plugin/src/components/Integrations/IntegrationTemplateBlock.module.scss b/grafana-plugin/src/components/Integrations/IntegrationTemplateBlock.module.scss new file mode 100644 index 00000000..12c604e3 --- /dev/null +++ b/grafana-plugin/src/components/Integrations/IntegrationTemplateBlock.module.scss @@ -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; + } +} diff --git a/grafana-plugin/src/components/Integrations/IntegrationTemplateBlock.tsx b/grafana-plugin/src/components/Integrations/IntegrationTemplateBlock.tsx new file mode 100644 index 00000000..8882af94 --- /dev/null +++ b/grafana-plugin/src/components/Integrations/IntegrationTemplateBlock.tsx @@ -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 = ({ + label, + labelTooltip, + renderInput, + onEdit, + onRemove, + isLoading, +}) => { + let inlineLabelProps = { labelTooltip }; + if (!labelTooltip) { + delete inlineLabelProps.labelTooltip; + } + + return ( +
+ + {label} + +
+ {renderInput()} + +
+
+ ); +}; + +export default IntegrationTemplateBlock; diff --git a/grafana-plugin/src/components/Tag/Tag.module.css b/grafana-plugin/src/components/Tag/Tag.module.css index c3dfbc1e..c8858482 100644 --- a/grafana-plugin/src/components/Tag/Tag.module.css +++ b/grafana-plugin/src/components/Tag/Tag.module.css @@ -3,4 +3,5 @@ line-height: 100%; padding: 5px 8px; color: white; + white-space: nowrap; } diff --git a/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss b/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss new file mode 100644 index 00000000..4fcfed5d --- /dev/null +++ b/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss @@ -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; + } +} diff --git a/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.tsx b/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx similarity index 70% rename from grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.tsx rename to grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx index 4ce63f5d..82e9b68f 100644 --- a/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.tsx @@ -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 - +
+
{channelFilter.filtering_term && ( - {IntegrationHelper.truncateLine(channelFilter.filtering_term)} + + {channelFilter.filtering_term} + )} - - +
+ +
setRouteIdForDeletion(channelFilterId)} /> - - +
+
} content={
- - {channelFilter.slack_channel?.display_name && ( - + + {IntegrationHelper.getChatOpsChannels(channelFilter).map((chatOpsChannel, key) => ( + Publish to ChatOps - {channelFilter.slack_channel.display_name} + {chatOpsChannel} - )} + ))} + Escalate to + {escalationChain?.name && ( )} + {!escalationChain?.name && ( - + +
+ +
+ + No Escalation chain + +
)}
-
+
} /> diff --git a/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.module.scss b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss similarity index 100% rename from grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.module.scss rename to grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss diff --git a/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx similarity index 82% rename from grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx rename to grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx index cdd857dd..41446dd7 100644 --- a/grafana-plugin/src/pages/integration_2/ExpandedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx @@ -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 = 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) => ({ @@ -71,6 +82,9 @@ const ExpandedIntegrationRouteDisplay: React.FC @@ -101,31 +112,30 @@ const ExpandedIntegrationRouteDisplay: React.FC - - - Routing Template -
- + + Routing Template +
+ +
+
- -
-
+
+ + )} {routeIndex !== channelFiltersTotal.length - 1 && ( @@ -138,7 +148,7 @@ const ExpandedIntegrationRouteDisplay: React.FC )} - {hasChatOpsConnectors && ( + {(isSlackInstalled || isTelegramInstalled) && ( Publish to ChatOps diff --git a/grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.tsx b/grafana-plugin/src/containers/IntegrationContainers/Integration2HearbeatForm/Integration2HeartbeatForm.tsx similarity index 100% rename from grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.tsx rename to grafana-plugin/src/containers/IntegrationContainers/Integration2HearbeatForm/Integration2HeartbeatForm.tsx diff --git a/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.config.ts b/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.config.ts new file mode 100644 index 00000000..8ebc1958 --- /dev/null +++ b/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.config.ts @@ -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 }, + ], + }, +]; diff --git a/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.tsx b/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.tsx new file mode 100644 index 00000000..32e783f0 --- /dev/null +++ b/grafana-plugin/src/containers/IntegrationContainers/IntegrationTemplatesList.tsx @@ -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 = ({ + templates, + openEditTemplateModal, + alertReceiveChannelId, +}) => { + const { alertReceiveChannelStore } = useStore(); + const [isRestoringTemplate, setIsRestoringTemplate] = useState(false); + const [templateRestoreName, setTemplateRestoreName] = useState(undefined); + const [showConfirmModal, setShowConfirmModal] = useState(false); + + return ( +
+ {showConfirmModal && ( + onResetTemplate(templateRestoreName)} + onDismiss={() => onDismiss()} + /> + )} + + + + Templates are used to interpret alert from monitoring. Reduce noise, customize visualization + + + + {templatesToRender.map((template, key) => ( + + + {template.name && {template.name}} + + {template.contents.map((contents, innerKey) => ( + onShowConfirmModal(contents.name)} + label={contents.label} + renderInput={() => ( +
+ +
+ )} + onEdit={() => openEditTemplateModal(contents.name)} + /> + ))} +
+
+ ))} +
+ ); + + 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
{children}
; +}; + +export default IntegrationTemplateList; diff --git a/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.module.scss b/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.module.scss deleted file mode 100644 index 3eb80dda..00000000 --- a/grafana-plugin/src/pages/integration_2/CollapsedIntegrationRouteDisplay.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.spacing { - margin-bottom: 12px; -} diff --git a/grafana-plugin/src/pages/integration_2/Integration2.helper.ts b/grafana-plugin/src/pages/integration_2/Integration2.helper.ts index 0ed75b5f..7e12018e 100644 --- a/grafana-plugin/src/pages/integration_2/Integration2.helper.ts +++ b/grafana-plugin/src/pages/integration_2/Integration2.helper.ts @@ -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, routeIndex: number) { + getRouteConditionWording(channelFilters: Array, 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; diff --git a/grafana-plugin/src/pages/integration_2/Integration2.module.scss b/grafana-plugin/src/pages/integration_2/Integration2.module.scss index d3085883..b6ae4ea6 100644 --- a/grafana-plugin/src/pages/integration_2/Integration2.module.scss +++ b/grafana-plugin/src/pages/integration_2/Integration2.module.scss @@ -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; +} diff --git a/grafana-plugin/src/pages/integration_2/Integration2.tsx b/grafana-plugin/src/pages/integration_2/Integration2.tsx index ae63911c..58309bf8 100644 --- a/grafana-plugin/src/pages/integration_2/Integration2.tsx +++ b/grafana-plugin/src/pages/integration_2/Integration2.tsx @@ -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
{isTemplateSettingsOpen && ( this.setState({ isTemplateSettingsOpen: false })} @@ -197,63 +196,67 @@ class Integration2 extends React.Component )} - - {alertReceiveChannelCounter && ( - - - - )} +
+ + {alertReceiveChannelCounter && ( + + + + )} - - - {alertReceiveChannel.maintenance_till && ( - )} - {this.renderHearbeat(alertReceiveChannel)} + {alertReceiveChannel.maintenance_till && ( + + )} + + {this.renderHearbeat(alertReceiveChannel)} - - Type: - - {integration?.display_name} + Type: + + + {integration?.display_name} + + + + Team: + + + + Created by: + - - Team: - - - - Created by: - - - +
+
- - - Grouping: +
+
+ + Grouping: + {IntegrationHelper.truncateLine(templates['grouping_id_template'] || '')} - +
- - Autoresolve: +
+ + Autoresolve: + {IntegrationHelper.truncateLine(templates['resolve_condition_template'] || '')} - +
- - Visualisation: +
+ + Visualisation: + Multiple - - +
+
setIsHearbeatFormOpen(true)}> - Hearbeat + Hearbeat Settings
{!alertReceiveChannel.maintenance_till && ( @@ -899,13 +908,13 @@ const IntegrationActions: React.FC = ({ alertReceiveCha onClick={() => { setConfirmModal({ isOpen: true, - title: ( - <> + title: 'Delete Integration?', + body: ( + Are you sure you want to delete {' '} - integration? - + integration?{' '} + ), - body: <>This action cannot be undone., onConfirm: deleteIntegration, dismissText: 'Cancel', confirmText: 'Delete', diff --git a/grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.module.scss b/grafana-plugin/src/pages/integration_2/Integration2HeartbeatForm.module.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/grafana-plugin/src/pages/integration_2/IntegrationTemplateBlock.tsx b/grafana-plugin/src/pages/integration_2/IntegrationTemplateBlock.tsx deleted file mode 100644 index 6928f2de..00000000 --- a/grafana-plugin/src/pages/integration_2/IntegrationTemplateBlock.tsx +++ /dev/null @@ -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 = ({ - label, - labelTooltip, - renderInput, - showHelp, - onEdit, - onHelp, - onRemove, - isLoading, -}) => { - let inlineLabelProps = { labelTooltip }; - if (!labelTooltip) { - delete inlineLabelProps.labelTooltip; - } - - return ( - - - {label} - - {renderInput()} - - - )} - - {isLoading && } - - ); -}; - -export default IntegrationTemplateBlock; diff --git a/grafana-plugin/src/pages/integration_2/IntegrationTemplatesList.tsx b/grafana-plugin/src/pages/integration_2/IntegrationTemplatesList.tsx deleted file mode 100644 index 94487ff7..00000000 --- a/grafana-plugin/src/pages/integration_2/IntegrationTemplatesList.tsx +++ /dev/null @@ -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 = ({ - templates, - openEditTemplateModal, - alertReceiveChannelId, -}) => { - const { alertReceiveChannelStore } = useStore(); - const [isRestoringTemplate, setIsRestoringTemplate] = useState(false); - const [templateRestoreName, setTemplateRestoreName] = useState(undefined); - const [showConfirmModal, setShowConfirmModal] = useState(false); - - return ( -
- {showConfirmModal && ( - onResetTemplate(templateRestoreName)} - onDismiss={() => onDismiss()} - /> - )} - - - - Templates are used to interpret alert from monitoring. Reduce noise, customize visualization - - - - - - onShowConfirmModal('grouping_id_template')} - isLoading={isRestoringTemplate && templateRestoreName === 'grouping_id_template'} - label={'Grouping'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('grouping_id_template')} - /> - - onShowConfirmModal('resolve_condition_template')} - label={'Auto resolve'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('resolve_condition_template')} - /> -
-
- - - - Web - - onShowConfirmModal('web_title_template')} - label={'Title'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('web_title_template')} - /> - - onShowConfirmModal('web_message_template')} - label={'Message'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('web_message_template')} - /> - - onShowConfirmModal('web_image_url_template')} - label={'Image'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('web_image_url_template')} - /> -
-
- - - - onShowConfirmModal('acknowledge_condition_template')} - label={'Auto acknowledge'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('acknowledge_condition_template')} - /> - - onShowConfirmModal('source_link_template')} - label={'Source Link'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('source_link_template')} - /> -
-
- - - - onShowConfirmModal('phone_call_title_template')} - label={'Phone Call'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('phone_call_title_template')} - /> - - onShowConfirmModal('sms_title_template')} - label={'SMS'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('sms_title_template')} - /> -
-
- - - - Slack - - onShowConfirmModal('slack_title_template')} - label={'Title'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('slack_title_template')} - /> - - onShowConfirmModal('slack_message_template')} - label={'Message'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('slack_message_template')} - /> - - onShowConfirmModal('slack_image_url_template')} - label={'Image'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('slack_image_url_template')} - /> -
-
- - - - Telegram - onShowConfirmModal('telegram_title_template')} - label={'Title'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('telegram_title_template')} - /> - - onShowConfirmModal('telegram_message_template')} - label={'Message'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('telegram_message_template')} - /> - - onShowConfirmModal('telegram_image_url_template')} - label={'Image'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('telegram_image_url_template')} - /> -
-
- - - - Email - onShowConfirmModal('email_title_template')} - label={'Title'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('email_title_template')} - /> - - onShowConfirmModal('email_message_template')} - label={'Message'} - renderInput={() => ( -
- -
- )} - onEdit={() => openEditTemplateModal('email_message_template')} - /> -
-
-
- ); - - 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;