commit
ac9b82fb9f
29 changed files with 842 additions and 784 deletions
14
CHANGELOG.md
14
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
15
engine/engine/management/commands/create_sqlite_db.py
Normal file
15
engine/engine/management/commands/create_sqlite_db.py
Normal 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()
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
margin-bottom: 12px;
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -3,4 +3,5 @@
|
|||
line-height: 100%;
|
||||
padding: 5px 8px;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
.spacing {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
Loading…
Add table
Reference in a new issue