v1.3.69
This commit is contained in:
commit
bb3dc7ebdb
51 changed files with 192 additions and 195 deletions
|
|
@ -53,7 +53,7 @@ steps:
|
|||
- refs/tags/v*.*.*
|
||||
|
||||
- name: Lint Backend
|
||||
image: python:3.12.0
|
||||
image: python:3.11.4
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: settings.ci-test
|
||||
commands:
|
||||
|
|
@ -63,7 +63,7 @@ steps:
|
|||
- pre-commit run flake8 --all-files
|
||||
|
||||
- name: Unit Test Backend
|
||||
image: python:3.12.0
|
||||
image: python:3.11.4
|
||||
environment:
|
||||
RABBITMQ_URI: amqp://rabbitmq:rabbitmq@rabbit_test:5672
|
||||
DJANGO_SETTINGS_MODULE: settings.ci-test
|
||||
|
|
@ -379,4 +379,4 @@ kind: secret
|
|||
name: github_api_token
|
||||
---
|
||||
kind: signature
|
||||
hmac: 36a7d2e2906bad4f186adfa488057ffb9107ef1cdbb3e4d7cb61165b00870c6b
|
||||
hmac: b9e499a424faecd9a8f41552cc307bd3431cb0e3fac77f3ee99ce19258fc0fec
|
||||
|
|
|
|||
14
.github/workflows/linting-and-tests.yml
vendored
14
.github/workflows/linting-and-tests.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.0"
|
||||
python-version: "3.11.4"
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
engine/requirements.txt
|
||||
|
|
@ -117,7 +117,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.0"
|
||||
python-version: "3.11.4"
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
engine/requirements.txt
|
||||
|
|
@ -175,7 +175,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.0"
|
||||
python-version: "3.11.4"
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
engine/requirements.txt
|
||||
|
|
@ -225,7 +225,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.0"
|
||||
python-version: "3.11.4"
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
engine/requirements.txt
|
||||
|
|
@ -263,7 +263,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.0"
|
||||
python-version: "3.11.4"
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
engine/requirements.txt
|
||||
|
|
@ -282,7 +282,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.0"
|
||||
python-version: "3.11.4"
|
||||
cache: "pip"
|
||||
cache-dependency-path: tools/pagerduty-migrator/requirements.txt
|
||||
- name: Unit Test PD Migrator
|
||||
|
|
@ -298,7 +298,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.0"
|
||||
python-version: "3.11.4"
|
||||
cache: "pip"
|
||||
cache-dependency-path: |
|
||||
engine/requirements.txt
|
||||
|
|
|
|||
2
.github/workflows/snyk.yml
vendored
2
.github/workflows/snyk.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12.0"
|
||||
python-version: "3.11.4"
|
||||
cache: "pip"
|
||||
cache-dependency-path: engine/requirements.txt
|
||||
- uses: actions/setup-node@v3
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ repos:
|
|||
args: [--settings-file=dev/scripts/.isort.cfg, --filter-files]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.11.0
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
files: ^engine
|
||||
|
|
@ -29,7 +29,7 @@ repos:
|
|||
files: ^dev/scripts
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 6.1.0
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: ^engine
|
||||
|
|
|
|||
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
## v1.3.69 (2023-12-01)
|
||||
|
||||
Maintenance release + bugfixes
|
||||
|
||||
## v1.3.68 (2023-11-30)
|
||||
|
||||
### Fixed
|
||||
|
|
@ -27,11 +31,7 @@ Minor bugfixes + dependency updates :)
|
|||
|
||||
### Added
|
||||
|
||||
- Add options to customize table columns in AlertGroup page ([#3281](https://github.com/grafana/oncall/pull/3281))
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Python 3.12 by @joeyorlando ([#3456](https://github.com/grafana/oncall/pull/3456))
|
||||
- Add options to customize table columns in AlertGroup page ([3281](https://github.com/grafana/oncall/pull/3281))
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
@ -49,9 +49,9 @@ Minor bugfixes + dependency updates :)
|
|||
### Added
|
||||
|
||||
- Add ability to use Grafana Service Account Tokens for OnCall API (This is only enabled for resolution_notes
|
||||
endpoint currently) @mderynck ([#3189](https://github.com/grafana/oncall/pull/3189))
|
||||
endpoint currently) @mderynck ([#3189](https://github.com/grafana/oncall/pull/3189))
|
||||
- Add ability for webhook presets to mask sensitive headers @mderynck
|
||||
([#3189](https://github.com/grafana/oncall/pull/3189))
|
||||
([#3189](https://github.com/grafana/oncall/pull/3189))
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
@ -60,12 +60,13 @@ Minor bugfixes + dependency updates :)
|
|||
### Fixed
|
||||
|
||||
- Fixed issue that blocked saving webhooks with presets if the preset is controlling the URL @mderynck
|
||||
([#3189](https://github.com/grafana/oncall/pull/3189))
|
||||
([#3189](https://github.com/grafana/oncall/pull/3189))
|
||||
- User filter doesn't display current value on Alert Groups page ([1714](https://github.com/grafana/oncall/issues/1714))
|
||||
- Remove displaying rotation modal for Terraform/API based schedules
|
||||
- Filters polishing ([3183](https://github.com/grafana/oncall/issues/3183))
|
||||
- Fixed permissions so User settings reader role included list users @mderynck ([#3419](https://github.com/grafana/oncall/pull/3419))
|
||||
- Fixed alert group rendering when some links were broken because of replacing `-` to `_` @Ferril ([#3424](https://github.com/grafana/oncall/pull/3424))
|
||||
- Make telegram on_alert_group_action_triggered asynchronous([#3471](https://github.com/grafana/oncall/pull/3471))
|
||||
|
||||
## v1.3.62 (2023-11-21)
|
||||
|
||||
|
|
|
|||
8
Makefile
8
Makefile
|
|
@ -155,8 +155,12 @@ cleanup: stop ## this will remove all of the images, containers, volumes, and n
|
|||
docker system prune --filter label="$(DOCKER_COMPOSE_DEV_LABEL)" --all --volumes
|
||||
|
||||
install-pre-commit:
|
||||
echo "installing pre-commit"
|
||||
pip install $$(grep "pre-commit" $(ENGINE_DIR)/requirements-dev.txt)
|
||||
@if [ ! -x "$$(command -v pre-commit)" ]; then \
|
||||
echo "installing pre-commit"; \
|
||||
pip install $$(grep "pre-commit" $(ENGINE_DIR)/requirements-dev.txt); \
|
||||
else \
|
||||
echo "pre-commit already installed"; \
|
||||
fi
|
||||
|
||||
lint: install-pre-commit ## run both frontend and backend linters
|
||||
## may need to run `yarn install` from within `grafana-plugin`
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ Related: [How to develop integrations](/engine/config_integrations/README.md)
|
|||
```yaml
|
||||
env:
|
||||
- name: FEATURE_LABELS_ENABLED_FOR_ALL
|
||||
value: "True"
|
||||
value: 'True'
|
||||
```
|
||||
|
||||
3. Wait until all resources are green and open <http://localhost:3000/a/grafana-oncall-app> (user: oncall, password: oncall)
|
||||
|
|
@ -215,8 +215,8 @@ See the `django-silk` documentation [here](https://github.com/jazzband/django-si
|
|||
By default everything runs inside Docker. If you would like to run the backend services outside of Docker
|
||||
(for integrating w/ PyCharm for example), follow these instructions:
|
||||
|
||||
1. Create a Python 3.12 virtual environment using a method of your choosing (ex.
|
||||
[venv](https://docs.python.org/3.12/library/venv.html) or [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)).
|
||||
1. Create a Python 3.11 virtual environment using a method of your choosing (ex.
|
||||
[venv](https://docs.python.org/3.11/library/venv.html) or [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)).
|
||||
Make sure the virtualenv is "activated".
|
||||
2. `postgres` is a dependency on some of our Python dependencies (notably `psycopg2`
|
||||
([docs](https://www.psycopg.org/docs/install.html#prerequisites))). Please visit
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ capable of generating the following objects:
|
|||
|
||||
## Prerequisites
|
||||
|
||||
1. Create/active a Python 3.12 virtual environment
|
||||
1. Create/active a Python 3.11 virtual environment
|
||||
2. `pip install -r requirements.txt`
|
||||
3. Must have a local version of Grafana and OnCall up and running
|
||||
4. Generate an API key inside of Grafana OnCall
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.12.0-alpine3.18 AS base
|
||||
FROM python:3.11.4-alpine3.18 AS base
|
||||
|
||||
# Create a group and user to run an app
|
||||
ENV APP_USER=appuser
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class GrafanaAlertingSyncManager:
|
|||
if config is None:
|
||||
logger.warning(
|
||||
f"GrafanaAlertingSyncManager: Got config None in get_alerting_config_for_datasource "
|
||||
f"for is_grafana_datasource {datasource_uid == cls.GRAFANA_ALERTING_DATASOURCE}, "
|
||||
f"for is_grafana_datasource {datasource_uid==cls.GRAFANA_ALERTING_DATASOURCE}, "
|
||||
f"response: {response_info}"
|
||||
)
|
||||
return
|
||||
|
|
@ -232,7 +232,7 @@ class GrafanaAlertingSyncManager:
|
|||
if response is None:
|
||||
logger.warning(
|
||||
f"GrafanaAlertingSyncManager: Failed to update contact point (POST) for is_grafana_datasource "
|
||||
f"{datasource_uid == cls.GRAFANA_ALERTING_DATASOURCE}; response: {response_info}"
|
||||
f"{datasource_uid==cls.GRAFANA_ALERTING_DATASOURCE}; response: {response_info}"
|
||||
)
|
||||
if response_info.get("status_code") == status.HTTP_400_BAD_REQUEST:
|
||||
logger.warning(f"GrafanaAlertingSyncManager: Config: {config}, Updated config: {updated_config}")
|
||||
|
|
|
|||
|
|
@ -1845,11 +1845,11 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
result_log_report = list()
|
||||
|
||||
for log_record in log_records_list:
|
||||
if type(log_record) is AlertGroupLogRecord:
|
||||
if type(log_record) == AlertGroupLogRecord:
|
||||
result_log_report.append(log_record.render_log_line_json())
|
||||
elif type(log_record) is UserNotificationPolicyLogRecord:
|
||||
elif type(log_record) == UserNotificationPolicyLogRecord:
|
||||
result_log_report.append(log_record.rendered_notification_log_line_json)
|
||||
elif type(log_record) is ResolutionNote:
|
||||
elif type(log_record) == ResolutionNote:
|
||||
result_log_report.append(log_record.render_log_line_json())
|
||||
return result_log_report
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from unittest.mock import call, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.utils import timezone
|
||||
|
|
@ -74,16 +74,10 @@ def test_direct_paging_user(make_organization, make_user_for_organization):
|
|||
assert alert.message == msg
|
||||
|
||||
# notifications sent
|
||||
notifications_sent = ((user, False), (other_user, True))
|
||||
|
||||
notify_task.apply_async.assert_has_calls(
|
||||
[
|
||||
call((u.pk, ag.pk), {"important": important, "notify_even_acknowledged": True, "notify_anyway": True})
|
||||
for u, important in notifications_sent
|
||||
]
|
||||
)
|
||||
|
||||
for u, important in notifications_sent:
|
||||
for u, important in ((user, False), (other_user, True)):
|
||||
assert notify_task.apply_async.called_with(
|
||||
(u.pk, ag.pk), {"important": important, "notify_even_acknowledged": True, "notify_anyway": True}
|
||||
)
|
||||
expected_info = {"user": u.public_primary_key, "important": important}
|
||||
assert_log_record(ag, f"{from_user.username} paged user {u.username}", expected_info=expected_info)
|
||||
|
||||
|
|
@ -179,7 +173,7 @@ def test_direct_paging_reusing_alert_group(
|
|||
|
||||
# notifications sent
|
||||
ag = alert_groups.get()
|
||||
notify_task.apply_async.assert_called_with(
|
||||
assert notify_task.apply_async.called_with(
|
||||
(user.pk, ag.pk), {"important": False, "notify_even_acknowledged": True, "notify_anyway": True}
|
||||
)
|
||||
|
||||
|
|
@ -249,17 +243,11 @@ def test_direct_paging_always_create_group(make_organization, make_user_for_orga
|
|||
assert alert_groups.count() == 2
|
||||
|
||||
# notifications sent
|
||||
notify_task.apply_async.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
(user.pk, alert_groups[0].pk),
|
||||
{"important": False, "notify_even_acknowledged": True, "notify_anyway": True},
|
||||
),
|
||||
call(
|
||||
(user.pk, alert_groups[1].pk),
|
||||
{"important": False, "notify_even_acknowledged": True, "notify_anyway": True},
|
||||
),
|
||||
]
|
||||
assert notify_task.apply_async.called_with(
|
||||
(user.pk, alert_groups[0].pk), {"important": False, "notify_even_acknowledged": True, "notify_anyway": True}
|
||||
)
|
||||
assert notify_task.apply_async.called_with(
|
||||
(user.pk, alert_groups[1].pk), {"important": False, "notify_even_acknowledged": True, "notify_anyway": True}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -85,10 +85,10 @@ def test_list_alert_receive_channel_skip_pagination_for_grafana_alerting(
|
|||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
if should_be_unpaginated:
|
||||
assert type(results) is list
|
||||
assert type(results) == list
|
||||
assert len(results) > 0
|
||||
else:
|
||||
assert type(results["results"]) is list
|
||||
assert type(results["results"]) == list
|
||||
assert len(results["results"]) > 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ class AlertReceiveChannelView(
|
|||
if payload is None:
|
||||
return channel.alert_groups.last().alerts.first()
|
||||
else:
|
||||
if type(payload) is not dict:
|
||||
if type(payload) != dict:
|
||||
raise PreviewTemplateException("Payload must be a valid json object")
|
||||
# Build Alert and AlertGroup objects to pass to templater without saving them to db
|
||||
alert_group_to_template = AlertGroup(channel=channel)
|
||||
|
|
@ -336,7 +336,7 @@ class AlertReceiveChannelView(
|
|||
try:
|
||||
instance.start_maintenance(mode, duration, request.user)
|
||||
except MaintenanceCouldNotBeStartedError as e:
|
||||
if type(instance) is AlertReceiveChannel:
|
||||
if type(instance) == AlertReceiveChannel:
|
||||
detail = {"alert_receive_channel_id": ["Already on maintenance"]}
|
||||
else:
|
||||
detail = str(e)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ def test_multi_type_support(value):
|
|||
LiveSetting.objects.create(name="SOME_NEW_FEATURE_ENABLED", value=value)
|
||||
setting_value = LiveSetting.get_setting("SOME_NEW_FEATURE_ENABLED")
|
||||
|
||||
assert type(setting_value) is type(value)
|
||||
assert type(setting_value) == type(value)
|
||||
assert setting_value == value
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class TestIsRbacEnabledForStack:
|
|||
|
||||
api_client = GcomAPIClient("someFakeApiToken")
|
||||
assert api_client.is_rbac_enabled_for_stack(stack_id) == expected
|
||||
mocked_gcom_api_client_api_get.assert_called_once_with(f"instances/{stack_id}?config=true")
|
||||
assert mocked_gcom_api_client_api_get.called_once_with(f"instances/{stack_id}?config=true")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"instance_info_feature_toggles,delimiter,expected",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ def test_it_triggers_an_organization_sync_and_saves_the_grafana_token(
|
|||
response = client.post(reverse("grafana-plugin:install"), format="json", **auth_headers)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
mocked_sync_organization.assert_called_once_with(organization)
|
||||
assert mocked_sync_organization.called_once_with(organization)
|
||||
|
||||
# make sure api token is saved on the org
|
||||
organization.refresh_from_db()
|
||||
|
|
|
|||
|
|
@ -74,8 +74,8 @@ def test_it_properly_handles_errors_from_the_grafana_api(
|
|||
url = reverse("grafana-plugin:self-hosted-install")
|
||||
response = client.post(url, format="json", **make_self_hosted_install_header(GRAFANA_TOKEN))
|
||||
|
||||
mocked_grafana_api_client.assert_called_once_with(api_url=GRAFANA_API_URL, api_token=GRAFANA_TOKEN)
|
||||
mocked_grafana_api_client.return_value.check_token.assert_called_once_with()
|
||||
assert mocked_grafana_api_client.called_once_with(api_url=GRAFANA_API_URL, api_token=GRAFANA_TOKEN)
|
||||
assert mocked_grafana_api_client.return_value.check_token.called_once_with()
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.data["error"] == expected_error_msg
|
||||
|
|
@ -106,13 +106,13 @@ def test_if_organization_exists_it_is_updated(
|
|||
url = reverse("grafana-plugin:self-hosted-install")
|
||||
response = client.post(url, format="json", **make_self_hosted_install_header(GRAFANA_TOKEN))
|
||||
|
||||
mocked_grafana_api_client.assert_called_once_with(api_url=GRAFANA_API_URL, api_token=GRAFANA_TOKEN)
|
||||
mocked_grafana_api_client.return_value.check_token.assert_called_once_with()
|
||||
mocked_grafana_api_client.return_value.is_rbac_enabled_for_organization.assert_called_once_with()
|
||||
assert mocked_grafana_api_client.called_once_with(api_url=GRAFANA_API_URL, api_token=GRAFANA_TOKEN)
|
||||
assert mocked_grafana_api_client.return_value.check_token.called_once_with()
|
||||
assert mocked_grafana_api_client.return_value.is_rbac_enabled_for_organization.called_once_with()
|
||||
|
||||
mocked_sync_organization.assert_called_once_with(organization)
|
||||
mocked_provision_plugin.assert_called_once_with()
|
||||
mocked_revoke_plugin.assert_called_once_with()
|
||||
assert mocked_sync_organization.called_once_with(organization)
|
||||
assert mocked_provision_plugin.called_once_with()
|
||||
assert mocked_revoke_plugin.called_once_with()
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data == {"error": None, **provision_plugin_response}
|
||||
|
|
@ -151,12 +151,12 @@ def test_if_organization_does_not_exist_it_is_created(
|
|||
|
||||
organization = Organization.objects.filter(stack_id=STACK_ID, org_id=ORG_ID).first()
|
||||
|
||||
mocked_grafana_api_client.assert_called_once_with(api_url=GRAFANA_API_URL, api_token=GRAFANA_TOKEN)
|
||||
mocked_grafana_api_client.return_value.check_token.assert_called_once_with()
|
||||
mocked_grafana_api_client.return_value.is_rbac_enabled_for_organization.assert_called_once_with()
|
||||
assert mocked_grafana_api_client.called_once_with(api_url=GRAFANA_API_URL, api_token=GRAFANA_TOKEN)
|
||||
assert mocked_grafana_api_client.return_value.check_token.called_once_with()
|
||||
assert mocked_grafana_api_client.return_value.is_rbac_enabled_for_organization.called_once_with()
|
||||
|
||||
mocked_sync_organization.assert_called_once_with(organization)
|
||||
mocked_provision_plugin.assert_called_once_with()
|
||||
assert mocked_sync_organization.called_once_with(organization)
|
||||
assert mocked_provision_plugin.called_once_with()
|
||||
assert not mocked_revoke_plugin.called
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import pytest
|
|||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.db import OperationalError
|
||||
from django.urls import reverse
|
||||
from pytest_django import DjangoDbBlocker
|
||||
from pytest_django.plugin import _DatabaseBlocker
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
|
@ -12,12 +12,9 @@ from apps.alerts.models import AlertReceiveChannel
|
|||
from apps.integrations.mixins import AlertChannelDefiningMixin
|
||||
|
||||
|
||||
class DatabaseBlocker(DjangoDbBlocker):
|
||||
class DatabaseBlocker(_DatabaseBlocker):
|
||||
"""Customize pytest_django db blocker to raise OperationalError exception."""
|
||||
|
||||
def __init__(self, *args) -> None:
|
||||
super().__init__(_ispytest=True)
|
||||
|
||||
def _blocking_wrapper(*args, **kwargs):
|
||||
__tracebackhide__ = True
|
||||
__tracebackhide__ # Silence pyflakes
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ def setup_heartbeat_integration(name=None):
|
|||
}
|
||||
)
|
||||
else:
|
||||
setup_heartbeat_integration(f"{name} {random.randint(1, 1024)}")
|
||||
setup_heartbeat_integration(f"{name} { random.randint(1, 1024)}")
|
||||
except requests.Timeout:
|
||||
logger.warning("Unable to create cloud heartbeat integration. Request timeout.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ def test_notify_by_provider_call_limits_warning(
|
|||
phone_backend = PhoneBackend()
|
||||
phone_backend._notify_by_provider_call(user, "some_message")
|
||||
|
||||
mock_add_call_limit_warning.assert_called_once_with(2, "some_message")
|
||||
assert mock_add_call_limit_warning.called_once_with(2, "some_message")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ def test_notify_by_provider_sms_limits_warning(
|
|||
phone_backend = PhoneBackend()
|
||||
phone_backend._notify_by_provider_sms(user, "some_message")
|
||||
|
||||
mock_add_sms_limit_warning.assert_called_once_with(2, "some_message")
|
||||
assert mock_add_sms_limit_warning.called_once_with(2, "some_message")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ def test_export_calendar(make_organization_and_user_with_token, make_user_for_or
|
|||
|
||||
cal = Calendar.from_ical(response.data)
|
||||
|
||||
assert type(cal) is Calendar
|
||||
assert type(cal) == Calendar
|
||||
# check there are events
|
||||
assert len(cal.subcomponents) > 0
|
||||
for component in cal.walk():
|
||||
|
|
@ -112,7 +112,7 @@ def test_export_user_calendar(make_organization_and_user_with_token, make_schedu
|
|||
|
||||
cal = Calendar.from_ical(response.data)
|
||||
|
||||
assert type(cal) is Calendar
|
||||
assert type(cal) == Calendar
|
||||
assert cal.get("x-wr-calname") == "On-Call Schedule for {0}".format(user.username)
|
||||
assert cal.get("x-wr-timezone") == "UTC"
|
||||
assert cal.get("calscale") == "GREGORIAN"
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ def list_of_oncall_shifts_from_ical(
|
|||
pytz_tz = pytz.timezone("UTC")
|
||||
return (
|
||||
datetime.datetime.combine(e["start"], datetime.datetime.min.time(), tzinfo=pytz_tz)
|
||||
if type(e["start"]) is datetime.date
|
||||
if type(e["start"]) == datetime.date
|
||||
else e["start"]
|
||||
)
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ def get_shifts_dict(
|
|||
)
|
||||
# Define on-call shift out of ical event that has the actual user
|
||||
if len(users) > 0 or with_empty_shifts:
|
||||
if type(event[ICAL_DATETIME_START].dt) is datetime.date:
|
||||
if type(event[ICAL_DATETIME_START].dt) == datetime.date:
|
||||
start = event[ICAL_DATETIME_START].dt
|
||||
end = event[ICAL_DATETIME_END].dt
|
||||
result_date.append(
|
||||
|
|
@ -623,7 +623,7 @@ def is_icals_equal(first, second):
|
|||
def ical_date_to_datetime(date, tz, start):
|
||||
datetime_to_combine = datetime.time.min
|
||||
all_day = False
|
||||
if type(date) is datetime.date:
|
||||
if type(date) == datetime.date:
|
||||
all_day = True
|
||||
calendar_timezone_offset = datetime.datetime.now().astimezone(tz).utcoffset()
|
||||
date = datetime.datetime.combine(date, datetime_to_combine).astimezone(tz) - calendar_timezone_offset
|
||||
|
|
@ -776,7 +776,7 @@ def start_end_with_respect_to_all_day(event: IcalEvent, calendar_tz):
|
|||
|
||||
def event_start_end_all_day_with_respect_to_type(event: IcalEvent, calendar_tz):
|
||||
all_day = False
|
||||
if type(event[ICAL_DATETIME_START].dt) is datetime.date:
|
||||
if type(event[ICAL_DATETIME_START].dt) == datetime.date:
|
||||
start, end = start_end_with_respect_to_all_day(event, calendar_tz)
|
||||
all_day = True
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ class OnCallSchedule(PolymorphicModel):
|
|||
events: ScheduleEvents = []
|
||||
for shift in shifts:
|
||||
start = shift["start"]
|
||||
all_day = type(start) is datetime.date
|
||||
all_day = type(start) == datetime.date
|
||||
# fix confusing end date for all-day event
|
||||
end = shift["end"] - datetime.timedelta(days=1) if all_day else shift["end"]
|
||||
if all_day and all_day_datetime:
|
||||
|
|
@ -500,7 +500,7 @@ class OnCallSchedule(PolymorphicModel):
|
|||
# check if event was ended or cancelled, update ical
|
||||
dtend = component.get(ICAL_DATETIME_END)
|
||||
dtend_datetime = dtend.dt if dtend else None
|
||||
if dtend_datetime and type(dtend_datetime) is datetime.date:
|
||||
if dtend_datetime and type(dtend_datetime) == datetime.date:
|
||||
# shift or overrides coming from ical calendars can be all day events, change to datetime
|
||||
dtend_datetime = datetime.datetime.combine(
|
||||
dtend.dt, datetime.datetime.min.time(), tzinfo=pytz.UTC
|
||||
|
|
@ -1120,7 +1120,6 @@ class OnCallScheduleICal(OnCallSchedule):
|
|||
|
||||
|
||||
class OnCallScheduleCalendar(OnCallSchedule):
|
||||
custom_on_call_shifts: "RelatedManager['CustomOnCallShift']"
|
||||
escalation_policies: "RelatedManager['EscalationPolicy']"
|
||||
objects: models.Manager["OnCallScheduleCalendar"]
|
||||
schedule_export_token: "RelatedManager['ScheduleExportAuthToken']"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ def test_soft_delete(shift_swap_request_setup):
|
|||
ssr.refresh_from_db()
|
||||
assert ssr.deleted_at is not None
|
||||
|
||||
mock_refresh_final.apply_async.assert_called_with((ssr.schedule.pk,))
|
||||
assert mock_refresh_final.apply_async.called_with((ssr.schedule.pk,))
|
||||
|
||||
assert ShiftSwapRequest.objects.all().count() == 0
|
||||
assert ShiftSwapRequest.objects_with_deleted.all().count() == 1
|
||||
|
|
@ -100,7 +100,7 @@ def test_take(
|
|||
mock_notify_beneficiary_about_taken_shift_swap_request.apply_async.assert_called_once_with((ssr.pk,))
|
||||
|
||||
# final schedule refresh was triggered
|
||||
mock_refresh_final.apply_async.assert_called_with((ssr.schedule.pk,))
|
||||
assert mock_refresh_final.apply_async.called_with((ssr.schedule.pk,))
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ class SlackChannelMessageEventStep(scenario_step.ScenarioStep):
|
|||
|
||||
STEPS_ROUTING: ScenarioRoute.RoutingSteps = [
|
||||
typing.cast(
|
||||
ScenarioRoute.EventCallbackScenarioRoute,
|
||||
ScenarioRoute.EventCallbackChannelMessageScenarioRoute,
|
||||
{
|
||||
"payload_type": PayloadType.EVENT_CALLBACK,
|
||||
"event_type": EventType.MESSAGE,
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ class AlertGroupLogSlackRenderer:
|
|||
# get rendered logs
|
||||
result = ""
|
||||
for log_record in all_log_records: # list of AlertGroupLogRecord and UserNotificationPolicyLogRecord logs
|
||||
if type(log_record) is AlertGroupLogRecord:
|
||||
if type(log_record) == AlertGroupLogRecord:
|
||||
result += f"{log_record.rendered_incident_log_line(for_slack=True)}\n"
|
||||
elif type(log_record) is UserNotificationPolicyLogRecord:
|
||||
elif type(log_record) == UserNotificationPolicyLogRecord:
|
||||
result += f"{log_record.rendered_notification_log_line(for_slack=True)}\n"
|
||||
|
||||
attachments.append(
|
||||
|
|
|
|||
|
|
@ -242,9 +242,7 @@ def test_trigger_paging_additional_responders(make_organization_and_user_with_sl
|
|||
with patch.object(step._slack_client, "api_call"):
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
mock_direct_paging.assert_called_once_with(
|
||||
organization=organization, from_user=user, message="The Message", team=team, users=[(user, True)]
|
||||
)
|
||||
mock_direct_paging.called_once_with(organization, user, "The Message", team, [(user, True)])
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -258,13 +256,7 @@ def test_page_team(make_organization_and_user_with_slack_identities, make_team):
|
|||
with patch.object(step._slack_client, "api_call"):
|
||||
step.process_scenario(slack_user_identity, slack_team_identity, payload)
|
||||
|
||||
mock_direct_paging.assert_called_once_with(
|
||||
organization=organization,
|
||||
from_user=user,
|
||||
message="The Message",
|
||||
team=team,
|
||||
users=[],
|
||||
)
|
||||
mock_direct_paging.called_once_with(organization, user, "The Message", team)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -19,7 +19,17 @@ class ScenarioRoute:
|
|||
class EventCallbackScenarioRoute(_Base):
|
||||
payload_type: typing.Literal[PayloadType.EVENT_CALLBACK]
|
||||
event_type: EventType
|
||||
message_channel_type: typing.NotRequired[typing.Literal[EventType.MESSAGE_CHANNEL]]
|
||||
|
||||
class EventCallbackChannelMessageScenarioRoute(EventCallbackScenarioRoute):
|
||||
"""
|
||||
NOTE: the reason why we need to subclass `EventCallbackScenarioRoute` is because in Python 3.11 there is currently
|
||||
no way to specify keys as optional in a `typing.TypedDict`. See [PEP-692](https://peps.python.org/pep-0692/) which
|
||||
will implement this typing feature in Python 3.12.
|
||||
|
||||
When we upgrade to 3.12 we should update this type.
|
||||
"""
|
||||
|
||||
message_channel_type: typing.Literal[EventType.MESSAGE_CHANNEL]
|
||||
|
||||
class InteractiveMessageScenarioRoute(_Base):
|
||||
payload_type: typing.Literal[PayloadType.INTERACTIVE_MESSAGE]
|
||||
|
|
@ -41,6 +51,7 @@ class ScenarioRoute:
|
|||
RoutingStep = (
|
||||
BlockActionsScenarioRoute
|
||||
| EventCallbackScenarioRoute
|
||||
| EventCallbackChannelMessageScenarioRoute
|
||||
| InteractiveMessageScenarioRoute
|
||||
| MessageActionScenarioRoute
|
||||
| SlashCommandScenarioRoute
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import logging
|
|||
from apps.alerts.models import AlertGroup
|
||||
from apps.alerts.representative import AlertGroupAbstractRepresentative
|
||||
from apps.telegram.models import TelegramMessage
|
||||
from apps.telegram.tasks import edit_message, on_create_alert_telegram_representative_async
|
||||
from apps.telegram.tasks import (
|
||||
edit_message,
|
||||
on_alert_group_action_triggered_async,
|
||||
on_create_alert_telegram_representative_async,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
|
@ -78,15 +82,11 @@ class AlertGroupTelegramRepresentative(AlertGroupAbstractRepresentative):
|
|||
from apps.alerts.models import AlertGroupLogRecord
|
||||
|
||||
log_record = kwargs["log_record"]
|
||||
logger.info(f"AlertGroupTelegramRepresentative ACTION SIGNAL, log record {log_record}")
|
||||
|
||||
if not isinstance(log_record, AlertGroupLogRecord):
|
||||
log_record = AlertGroupLogRecord.objects.get(pk=log_record)
|
||||
|
||||
instance = cls(log_record)
|
||||
if instance.is_applicable():
|
||||
handler = instance.get_handler()
|
||||
handler()
|
||||
if isinstance(log_record, AlertGroupLogRecord):
|
||||
log_record_id = log_record.pk
|
||||
else:
|
||||
log_record_id = log_record
|
||||
on_alert_group_action_triggered_async.apply_async((log_record_id,))
|
||||
|
||||
@staticmethod
|
||||
def on_create_alert(**kwargs):
|
||||
|
|
|
|||
|
|
@ -215,3 +215,21 @@ def on_create_alert_telegram_representative_async(self, alert_pk):
|
|||
)
|
||||
for message in messages_to_edit:
|
||||
edit_message.delay(message_pk=message.pk)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(
|
||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
|
||||
)
|
||||
def on_alert_group_action_triggered_async(log_record_id):
|
||||
from apps.alerts.models import AlertGroupLogRecord
|
||||
|
||||
from .alert_group_representative import AlertGroupTelegramRepresentative
|
||||
|
||||
logger.info(f"AlertGroupTelegramRepresentative ACTION SIGNAL, log record {log_record_id}")
|
||||
|
||||
log_record = AlertGroupLogRecord.objects.get(pk=log_record_id)
|
||||
|
||||
instance = AlertGroupTelegramRepresentative(log_record)
|
||||
if instance.is_applicable():
|
||||
handler = instance.get_handler()
|
||||
handler()
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ def test_organization_moved_middleware_amazon_sns_headers(
|
|||
response = client.post(url, data, format="json", **expected_sns_headers)
|
||||
assert mocked_make_request.called
|
||||
for k in AMAZON_SNS_HEADERS:
|
||||
assert expected_sns_headers.get(f'HTTP_{k.upper().replace("-", "_")}') == mocked_make_request.call_args.args[
|
||||
assert expected_sns_headers.get(f'HTTP_{k.upper().replace("-","_")}') == mocked_make_request.call_args.args[
|
||||
2
|
||||
].get(k)
|
||||
assert response.content == expected_message
|
||||
|
|
|
|||
|
|
@ -368,8 +368,8 @@ def test_sync_organization_is_rbac_permissions_enabled_cloud(mocked_gcom_client,
|
|||
|
||||
organization.refresh_from_db()
|
||||
|
||||
mocked_gcom_client.assert_called_once_with("mockedToken")
|
||||
mocked_gcom_client.return_value.is_rbac_enabled_for_stack.assert_called_once_with(stack_id)
|
||||
assert mocked_gcom_client.return_value.called_once_with("mockedToken")
|
||||
assert mocked_gcom_client.return_value.is_rbac_enabled_for_stack.called_once_with(stack_id)
|
||||
assert organization.is_rbac_permissions_enabled == gcom_api_response
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ def isoformat_with_tz_suffix(value):
|
|||
|
||||
|
||||
def is_string_with_visible_characters(string):
|
||||
return type(string) is str and not string.isspace() and not string == ""
|
||||
return type(string) == str and not string.isspace() and not string == ""
|
||||
|
||||
|
||||
def str_or_backup(string, backup):
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
profile = "black"
|
||||
line_length=120
|
||||
float_to_top=true
|
||||
# TODO: update py_version to 312 once isort supports it
|
||||
py_version=311
|
||||
extend_skip_glob = "**/migrations/**"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ["py312"]
|
||||
target-version = ["py311"]
|
||||
force-exclude = "migrations"
|
||||
|
||||
[tool.mypy]
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
celery-types==0.18.0
|
||||
django-filter-stubs==0.1.3
|
||||
django-stubs==4.2.6
|
||||
djangorestframework-stubs==3.14.4
|
||||
mypy==1.7.1
|
||||
pre-commit==3.5.0
|
||||
pytest==7.4.3
|
||||
pytest-django==4.7.0
|
||||
pytest_factoryboy==2.6.0
|
||||
django-stubs[compatible-mypy]==4.2.2
|
||||
djangorestframework-stubs[compatible-mypy]==3.14.2
|
||||
mypy==1.4.1
|
||||
pre-commit==2.15.0
|
||||
pytest==7.3.1
|
||||
pytest-django==4.5.2
|
||||
pytest_factoryboy==2.5.1
|
||||
types-beautifulsoup4==4.12.0.5
|
||||
types-PyMySQL==1.0.19.7
|
||||
types-python-dateutil==2.8.19.13
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ pymdown-extensions==10.0
|
|||
requests==2.31.0
|
||||
urllib3==1.26.18
|
||||
prometheus_client==0.16.0
|
||||
lxml==4.9.3
|
||||
lxml==4.9.2
|
||||
babel==2.12.1
|
||||
drf-spectacular==0.26.5
|
||||
grpcio==1.59.0
|
||||
grpcio==1.57.0
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ CELERY_TASK_ROUTES = {
|
|||
"apps.telegram.tasks.register_telegram_webhook": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.send_link_to_channel_message_or_fallback_to_full_alert_group": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.send_log_and_actions_message": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.on_alert_group_action_triggered_async": {"queue": "telegram"},
|
||||
# WEBHOOK
|
||||
"apps.alerts.tasks.custom_button_result.custom_button_result": {"queue": "webhook"},
|
||||
"apps.alerts.tasks.custom_webhook_result.custom_webhook_result": {"queue": "webhook"},
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.u-width-height-100 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.u-display-block {
|
||||
display: block;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ const MonacoEditor: FC<MonacoEditorProps> = (props) => {
|
|||
height={height}
|
||||
onEditorDidMount={handleMount}
|
||||
getSuggestions={useAutoCompleteList ? autoCompleteList : undefined}
|
||||
containerStyles="u-width-100"
|
||||
containerStyles="u-width-height-100"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,13 +68,11 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
|
|||
onOpenIntegraionSettings(id);
|
||||
};
|
||||
|
||||
const getInheritanceChangeHandler = (keyId: ApiSchemas['LabelKey']['id']) => {
|
||||
return (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAlertGroupLabels((alertGroupLabels) => ({
|
||||
...alertGroupLabels,
|
||||
inheritable: { ...alertGroupLabels.inheritable, [keyId]: event.target.checked },
|
||||
}));
|
||||
};
|
||||
const onInheritanceChange = (keyId: ApiSchemas['LabelKey']['id']) => {
|
||||
setAlertGroupLabels((alertGroupLabels) => ({
|
||||
...alertGroupLabels,
|
||||
inheritable: { ...alertGroupLabels.inheritable, [keyId]: !alertGroupLabels.inheritable[keyId] },
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -98,7 +96,7 @@ const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps) => {
|
|||
<InlineSwitch
|
||||
value={alertGroupLabels.inheritable[label.key.id]}
|
||||
transparent
|
||||
onChange={getInheritanceChangeHandler(label.key.id)}
|
||||
onChange={() => onInheritanceChange(label.key.id)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -65,3 +65,7 @@
|
|||
.template-block-codeeditor div[aria-label='Code editor container'] {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.template-editor-block-content {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { AlertTemplatesDTO } from 'models/alert_templates';
|
|||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { TemplateOptions } from 'pages/integration/Integration.config';
|
||||
import { waitForElement } from 'utils/DOM';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
|
|
@ -50,7 +49,6 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
const [alertGroupPayload, setAlertGroupPayload] = useState<JSON>(undefined);
|
||||
const [changedTemplateBody, setChangedTemplateBody] = useState<string>(templateBody);
|
||||
const [resultError, setResultError] = useState<string>(undefined);
|
||||
const [editorHeight, setEditorHeight] = useState<string>(undefined);
|
||||
const [isRecentAlertGroupExisting, setIsRecentAlertGroupExisting] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -63,14 +61,6 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
waitForElement('#content-container-id').then(() => {
|
||||
const mainDiv = document.getElementById('content-container-id');
|
||||
const height = mainDiv?.getBoundingClientRect().height - 59;
|
||||
setEditorHeight(`${height}px`);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onShowCheatSheet = useCallback(() => {
|
||||
setIsCheatSheetVisible(true);
|
||||
}, []);
|
||||
|
|
@ -188,7 +178,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
width={'95%'}
|
||||
>
|
||||
<div className={cx('container-wrapper')}>
|
||||
<div className={cx('container')} id={'content-container-id'}>
|
||||
<div className={cx('container')}>
|
||||
<TemplatesAlertGroupsList
|
||||
templatePage={TEMPLATE_PAGE.Integrations}
|
||||
alertReceiveChannelId={id}
|
||||
|
|
@ -241,7 +231,7 @@ const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
|||
value={changedTemplateBody}
|
||||
data={templates}
|
||||
showLineNumbers={true}
|
||||
height={editorHeight}
|
||||
height="100%"
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -40,11 +40,19 @@
|
|||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.alert-groups-editor {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.alert-groups-editor div[aria-label='Code editor container'] {
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.alert-groups-editor-withBadge {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.alert-groups-editor-withBadge div[aria-label='Code editor container'] {
|
||||
background-color: var(--box-background);
|
||||
border-bottom: none;
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ import { useStore } from 'state/useStore';
|
|||
import styles from './TemplatesAlertGroupsList.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
const HEADER_OF_CONTAINER_HEIGHT = 59;
|
||||
const BADGE_WITH_PADDINGS_HEIGHT = 42;
|
||||
|
||||
export enum TEMPLATE_PAGE {
|
||||
Integrations,
|
||||
|
|
@ -71,18 +69,6 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const getCodeEditorHeight = () => {
|
||||
const mainDiv = document.getElementById('alerts-content-container-id');
|
||||
const height = mainDiv?.getBoundingClientRect().height - HEADER_OF_CONTAINER_HEIGHT;
|
||||
return `${height}px`;
|
||||
};
|
||||
|
||||
const getCodeEditorHeightWithBadge = () => {
|
||||
const mainDiv = document.getElementById('alerts-content-container-id');
|
||||
const height = mainDiv?.getBoundingClientRect().height - HEADER_OF_CONTAINER_HEIGHT - BADGE_WITH_PADDINGS_HEIGHT;
|
||||
return `${height}px`;
|
||||
};
|
||||
|
||||
const getChangeHandler = () => {
|
||||
return debounce((value: string) => {
|
||||
onEditPayload(value);
|
||||
|
|
@ -158,7 +144,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
...MONACO_EDITABLE_CONFIG,
|
||||
readOnly: false,
|
||||
}}
|
||||
height={getCodeEditorHeight()}
|
||||
height="100%"
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -271,7 +257,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
<MonacoEditor
|
||||
value={JSON.stringify(selectedPayload, null, 4)}
|
||||
data={templates}
|
||||
height={getCodeEditorHeight()}
|
||||
height="100%"
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
useAutoCompleteList={false}
|
||||
|
|
@ -310,7 +296,7 @@ const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
|||
value={JSON.stringify(selectedPayload, null, 4)}
|
||||
data={undefined}
|
||||
disabled
|
||||
height={getCodeEditorHeightWithBadge()}
|
||||
height="100%"
|
||||
onChange={getChangeHandler()}
|
||||
useAutoCompleteList={false}
|
||||
language={MONACO_LANGUAGE.json}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, Drawer, HorizontalGroup, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
|
@ -13,7 +13,6 @@ import TemplateResult from 'containers/TemplateResult/TemplateResult';
|
|||
import TemplatesAlertGroupsList, { TEMPLATE_PAGE } from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { waitForElement } from 'utils/DOM';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -35,18 +34,9 @@ interface WebhooksTemplateEditorProps {
|
|||
const WebhooksTemplateEditor: React.FC<WebhooksTemplateEditorProps> = ({ template, id, onHide, handleSubmit }) => {
|
||||
const [isCheatSheetVisible, setIsCheatSheetVisible] = useState<boolean>(false);
|
||||
const [changedTemplateBody, setChangedTemplateBody] = useState<string>(template.value);
|
||||
const [editorHeight, setEditorHeight] = useState<string>(undefined);
|
||||
const [selectedPayload, setSelectedPayload] = useState(undefined);
|
||||
const [resultError, setResultError] = useState<string>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
waitForElement('#content-container-id').then(() => {
|
||||
const mainDiv = document.getElementById('content-container-id');
|
||||
const height = mainDiv?.getBoundingClientRect().height - 59;
|
||||
setEditorHeight(`${height}px`);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getChangeHandler = () => {
|
||||
return debounce((value: string) => {
|
||||
setChangedTemplateBody(value);
|
||||
|
|
@ -110,7 +100,7 @@ const WebhooksTemplateEditor: React.FC<WebhooksTemplateEditorProps> = ({ templat
|
|||
width="95%"
|
||||
>
|
||||
<div className={cx('container-wrapper')}>
|
||||
<div className={cx('container')} id={'content-container-id'}>
|
||||
<div className={cx('container')}>
|
||||
<TemplatesAlertGroupsList
|
||||
heading="Last events"
|
||||
templatePage={TEMPLATE_PAGE.Webhooks}
|
||||
|
|
@ -148,7 +138,7 @@ const WebhooksTemplateEditor: React.FC<WebhooksTemplateEditorProps> = ({ templat
|
|||
value={template.value}
|
||||
data={{ payload_example: selectedPayload }}
|
||||
showLineNumbers={true}
|
||||
height={editorHeight}
|
||||
height="100%"
|
||||
onChange={getChangeHandler()}
|
||||
suggestionPrefix=""
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -125,10 +125,10 @@ export class RootBaseStore {
|
|||
};
|
||||
|
||||
await retryFailingPromises([
|
||||
this.userStore.loadCurrentUser,
|
||||
this.organizationStore.loadCurrentOrganization,
|
||||
this.grafanaTeamStore.updateItems,
|
||||
updateFeatures,
|
||||
() => this.userStore.loadCurrentUser(),
|
||||
() => this.organizationStore.loadCurrentOrganization(),
|
||||
() => this.grafanaTeamStore.updateItems(),
|
||||
() => updateFeatures(),
|
||||
]);
|
||||
this.isBasicDataLoaded = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ describe('retryFailingPromises', () => {
|
|||
let attempts1 = 0;
|
||||
let attempts2 = 0;
|
||||
let attempts3 = 0;
|
||||
const fetch1 = async () => Promise.resolve(++attempts1);
|
||||
const fetch1 = (param = 'param') => {
|
||||
++attempts1;
|
||||
return Promise.resolve(param);
|
||||
};
|
||||
const fetch2 = async () => Promise.reject(++attempts2);
|
||||
const fetch3 = async () =>
|
||||
new Promise((resolve, reject) => {
|
||||
|
|
@ -19,13 +22,16 @@ describe('retryFailingPromises', () => {
|
|||
reject(attempts3);
|
||||
});
|
||||
|
||||
const result = await retryFailingPromises([fetch1, fetch2, fetch3], { maxAttempts: MAX_ATTEMPTS, delayInMs: 50 });
|
||||
const result = await retryFailingPromises([() => fetch1(), fetch2, fetch3], {
|
||||
maxAttempts: MAX_ATTEMPTS,
|
||||
delayInMs: 50,
|
||||
});
|
||||
|
||||
expect(attempts1).toBe(1);
|
||||
expect(attempts2).toBe(MAX_ATTEMPTS);
|
||||
expect(attempts3).toBe(2);
|
||||
expect(result).toEqual([
|
||||
{ status: 'fulfilled', value: 1 },
|
||||
{ status: 'fulfilled', value: 'param' },
|
||||
{ status: 'rejected', reason: 5 },
|
||||
{ status: 'fulfilled', value: 2 },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { retry } from '@lifeomic/attempt';
|
||||
import { AttemptContext, retry } from '@lifeomic/attempt';
|
||||
|
||||
export const retryFailingPromises = async (
|
||||
asyncActions: Array<() => Promise<unknown>>,
|
||||
asyncActions: Array<(ctx?: AttemptContext) => Promise<unknown>>,
|
||||
{ maxAttempts = 3, delayInMs = 500 }: { maxAttempts?: number; delayInMs?: number } = {}
|
||||
) =>
|
||||
maxAttempts === 0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.12.0-alpine
|
||||
FROM python:3.11.4-alpine
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
WORKDIR /app
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue