From 56683d2aa09a1df7d39447ee2ac456a4f20c6cce Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Thu, 13 Jun 2024 16:25:04 -0400 Subject: [PATCH 01/19] add release:ignore to update make docs procedure PRs --- .github/workflows/update-make-docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/update-make-docs.yml b/.github/workflows/update-make-docs.yml index 3e69b710..bde39d31 100644 --- a/.github/workflows/update-make-docs.yml +++ b/.github/workflows/update-make-docs.yml @@ -11,3 +11,5 @@ jobs: steps: - uses: actions/checkout@v4 - uses: grafana/writers-toolkit/update-make-docs@update-make-docs/v1 + with: + pr_options: --label "release:ignore" From bc1544cc01a5420e101014ff9793f2257159ebd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Jun 2024 20:30:35 +0000 Subject: [PATCH 02/19] Update `make docs` procedure (#4523) Co-authored-by: grafanabot Co-authored-by: Joey Orlando --- docs/docs.mk | 26 +++++++--- docs/make-docs | 134 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 123 insertions(+), 37 deletions(-) diff --git a/docs/docs.mk b/docs/docs.mk index a08830d9..c0aae10b 100644 --- a/docs/docs.mk +++ b/docs/docs.mk @@ -65,6 +65,11 @@ ifeq ($(origin HUGO_REFLINKSERRORLEVEL), undefined) export HUGO_REFLINKSERRORLEVEL := WARNING endif +# Whether to pull the latest container image before running the container. +ifeq ($(origin PULL), undefined) +export PULL := true +endif + .PHONY: docs-rm docs-rm: ## Remove the docs container. $(PODMAN) rm -f $(DOCS_CONTAINER) @@ -81,13 +86,12 @@ make-docs: fi .PHONY: docs -docs: ## Serve documentation locally, which includes pulling the latest `DOCS_IMAGE` (default: `grafana/docs-base:latest`) container image. See also `docs-no-pull`. +docs: ## Serve documentation locally, which includes pulling the latest `DOCS_IMAGE` (default: `grafana/docs-base:latest`) container image. To not pull the image, set `PULL=false`. +ifeq ($(PULL), true) docs: docs-pull make-docs - $(CURDIR)/make-docs $(PROJECTS) - -.PHONY: docs-no-pull -docs-no-pull: ## Serve documentation locally without pulling the `DOCS_IMAGE` (default: `grafana/docs-base:latest`) container image. -docs-no-pull: make-docs +else +docs: make-docs +endif $(CURDIR)/make-docs $(PROJECTS) .PHONY: docs-debug @@ -96,13 +100,19 @@ docs-debug: make-docs WEBSITE_EXEC='hugo server --bind 0.0.0.0 --port 3002 --debug' $(CURDIR)/make-docs $(PROJECTS) .PHONY: doc-validator -doc-validator: ## Run doc-validator on the entire docs folder. +doc-validator: ## Run doc-validator on the entire docs folder which includes pulling the latest `DOC_VALIDATOR_IMAGE` (default: `grafana/doc-validator:latest`) container image. To not pull the image, set `PULL=false`. doc-validator: make-docs +ifeq ($(PULL), true) + $(PODMAN) pull -q $(DOC_VALIDATOR_IMAGE) +endif DOCS_IMAGE=$(DOC_VALIDATOR_IMAGE) $(CURDIR)/make-docs $(PROJECTS) .PHONY: vale -vale: ## Run vale on the entire docs folder. +vale: ## Run vale on the entire docs folder which includes pulling the latest `VALE_IMAGE` (default: `grafana/vale:latest`) container image. To not pull the image, set `PULL=false`. vale: make-docs +ifeq ($(PULL), true) + $(PODMAN) pull -q $(VALE_IMAGE) +endif DOCS_IMAGE=$(VALE_IMAGE) $(CURDIR)/make-docs $(PROJECTS) .PHONY: update diff --git a/docs/make-docs b/docs/make-docs index 43efdb5f..f531df2e 100755 --- a/docs/make-docs +++ b/docs/make-docs @@ -6,6 +6,43 @@ # [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes. # Changes are relevant to this script and the support docs.mk GNU Make interface. # +# ## 8.0.0 (2024-05-28) +# +# ### Changed +# +# - Add environment variable `OUTPUT_FORMAT` to control the output of commands. +# +# The default value is `human` and means the output format is human readable. +# The value `json` is also supported and outputs JSON. +# +# Note that the `json` format isn't supported by `make docs`, only `make doc-validator` and `make vale`. +# +# ## 7.0.0 (2024-05-03) +# +# ### Changed +# +# - Pull images for all recipes that use containers by default. +# +# Use the `PULL=false` variable to disable this behavior. +# +# ### Removed +# +# - The `docs-no-pull` target as it's redundant with the new `PULL=false` variable. +# +# ## 6.1.0 (2024-04-22) +# +# ### Changed +# +# - Mount volumes with SELinux labels. +# +# https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label +# +# ### Added +# +# - Pseudo project for including only website resources and no website content. +# +# Facilitates testing shortcodes and layout changes with a small documentation set instead of Grafana Cloud or the entire website. +# # ## 6.0.1 (2024-02-28) # # ### Added @@ -229,6 +266,8 @@ readonly HUGO_REFLINKSERRORLEVEL="${HUGO_REFLINKSERRORLEVEL:-WARNING}" readonly VALE_MINALERTLEVEL="${VALE_MINALERTLEVEL:-error}" readonly WEBSITE_EXEC="${WEBSITE_EXEC:-make server-docs}" +readonly OUTPUT_FORMAT="${OUTPUT_FORMAT:-human}" + PODMAN="$(if command -v podman >/dev/null 2>&1; then echo podman; else echo docker; fi)" if ! command -v curl >/dev/null 2>&1; then @@ -300,6 +339,7 @@ SOURCES_helm_charts_tempo_distributed='tempo' SOURCES_opentelemetry='opentelemetry-docs' SOURCES_plugins_grafana_datadog_datasource='datadog-datasource' SOURCES_plugins_grafana_oracle_datasource='oracle-datasource' +SOURCES_resources='website' VERSIONS_as_code='UNVERSIONED' VERSIONS_grafana_cloud='UNVERSIONED' @@ -311,6 +351,7 @@ VERSIONS_grafana_cloud_frontend_observability_faro_web_sdk='UNVERSIONED' VERSIONS_opentelemetry='UNVERSIONED' VERSIONS_plugins_grafana_datadog_datasource='latest' VERSIONS_plugins_grafana_oracle_datasource='latest' +VERSIONS_resources='UNVERSIONED' VERSIONS_technical_documentation='UNVERSIONED' VERSIONS_website='UNVERSIONED' VERSIONS_writers_toolkit='UNVERSIONED' @@ -321,6 +362,7 @@ PATHS_helm_charts_tempo_distributed='docs/sources/helm-charts/tempo-distributed' PATHS_mimir='docs/sources/mimir' PATHS_plugins_grafana_datadog_datasource='docs/sources' PATHS_plugins_grafana_oracle_datasource='docs/sources' +PATHS_resources='content' PATHS_tempo='docs/sources/tempo' PATHS_website='content' @@ -584,6 +626,11 @@ POSIX_HERESTRING proj_to_url_src_dst_ver "$(new_proj helm-charts/mimir-distributed "${_version}")" proj_to_url_src_dst_ver "$(new_proj enterprise-metrics "${_version}")" ;; + resources) + _repo="$(repo_path website)" + echo "arbitrary^${_repo}/config^/hugo/config" "arbitrary^${_repo}/layouts^/hugo/layouts" "arbitrary^${_repo}/scripts^/hugo/scripts" + unset _repo + ;; traces) proj_to_url_src_dst_ver "$(new_proj tempo "${_version}")" proj_to_url_src_dst_ver "$(new_proj enterprise-traces "${_version}")" @@ -617,7 +664,7 @@ $x POSIX_HERESTRING if [ -n "${url}" ]; then - if [ "${_url}" != "arbitrary" ]; then + if [ "${url}" != arbitrary ]; then printf '\r %s\r\n' "${url}" fi fi @@ -670,9 +717,9 @@ POSIX_HERESTRING fi _repo="$(repo_path website)" - volumes="--volume=${_repo}/config:/hugo/config" - volumes="${volumes} --volume=${_repo}/layouts:/hugo/layouts" - volumes="${volumes} --volume=${_repo}/scripts:/hugo/scripts" + volumes="--volume=${_repo}/config:/hugo/config:z" + volumes="${volumes} --volume=${_repo}/layouts:/hugo/layouts:z" + volumes="${volumes} --volume=${_repo}/scripts:/hugo/scripts:z" fi unset _project _repo done @@ -682,7 +729,7 @@ for x in ${url_src_dst_vers}; do $x POSIX_HERESTRING - if [ "${_url}" != "arbitrary" ]; then + if [ "${_url}" != arbitrary ]; then if [ ! -f "${_src}/_index.md" ]; then errr "Index file '${_src}/_index.md' does not exist." note "Is '${_src}' the correct source directory?" @@ -693,9 +740,9 @@ POSIX_HERESTRING debg "Mounting '${_src}' at container path '${_dst}'" if [ -z "${volumes}" ]; then - volumes="--volume=${_src}:${_dst}" + volumes="--volume=${_src}:${_dst}:z" else - volumes="${volumes} --volume=${_src}:${_dst}" + volumes="${volumes} --volume=${_src}:${_dst}:z" fi if [ -n "${_ver}" ] && [ "${_ver}" != 'UNVERSIONED' ]; then @@ -714,45 +761,74 @@ POSIX_HERESTRING case "${image}" in 'grafana/doc-validator') - if ! command -v jq >/dev/null 2>&1; then - errr '`jq` must be installed for the `doc-validator` target to work.' - note 'To install `jq`, refer to https://jqlang.github.io/jq/download/,' - - exit 1 - fi - proj="$(new_proj "$1")" printf '\r\n' - "${PODMAN}" run \ + + IFS='' read -r cmd </dev/null 2>&1; then + errr '`jq` must be installed for the `doc-validator` target to work.' + note 'To install `jq`, refer to https://jqlang.github.io/jq/download/,' + + exit 1 + fi + + ${cmd} \ + | jq -r '"ERROR: \(.location.path):\(.location.range.start.line // 1):\(.location.range.start.column // 1): \(.message)" + if .suggestions[0].text then "\nSuggestion: \(.suggestions[0].text)" else "" end' + ;; + json) + ${cmd} + ;; + *) # default + errr "Invalid output format '${OUTPUT_FORMAT}'" + esac ;; 'grafana/vale') proj="$(new_proj "$1")" printf '\r\n' - "${PODMAN}" run \ + IFS='' read -r cmd < Date: Fri, 14 Jun 2024 15:46:14 +0800 Subject: [PATCH 03/19] Handle slack uninstall event from chatops-proxy (#4510) --- engine/apps/chatops_proxy/events/handlers.py | 38 ++++++++++++++-- .../apps/chatops_proxy/events/root_handler.py | 4 +- engine/apps/chatops_proxy/events/types.py | 6 +++ .../apps/chatops_proxy/tests/test_events.py | 43 +++++++++++++++---- 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/engine/apps/chatops_proxy/events/handlers.py b/engine/apps/chatops_proxy/events/handlers.py index 967be6ee..11897e99 100644 --- a/engine/apps/chatops_proxy/events/handlers.py +++ b/engine/apps/chatops_proxy/events/handlers.py @@ -3,10 +3,16 @@ import typing from abc import ABC, abstractmethod from apps.chatops_proxy.client import PROVIDER_TYPE_SLACK -from apps.slack.installation import SlackInstallationExc, install_slack_integration +from apps.slack.installation import SlackInstallationExc, install_slack_integration, uninstall_slack_integration from apps.user_management.models import Organization -from .types import INTEGRATION_INSTALLED_EVENT_TYPE, Event, IntegrationInstalledData +from .types import ( + INTEGRATION_INSTALLED_EVENT_TYPE, + INTEGRATION_UNINSTALLED_EVENT_TYPE, + Event, + IntegrationInstalledData, + IntegrationUninstalledData, +) logger = logging.getLogger(__name__) @@ -23,7 +29,7 @@ class Handler(ABC): pass -class SlackInstallationHandler(Handler): +class SlackInstallHandler(Handler): @classmethod def match(cls, event: Event) -> bool: return ( @@ -48,3 +54,29 @@ class SlackInstallationHandler(Handler): f'msg="SlackInstallationHandler: Failed to install Slack integration: %s" org_id={organization.id} stack_id={stack_id}', e, ) + + +class SlackUninstallHandler(Handler): + @classmethod + def match(cls, event: Event) -> bool: + return ( + event.get("event_type") == INTEGRATION_UNINSTALLED_EVENT_TYPE + and event.get("data", {}).get("provider_type") == PROVIDER_TYPE_SLACK + ) + + @classmethod + def handle(cls, data: dict) -> None: + data = typing.cast(IntegrationUninstalledData, data) + + stack_id = data.get("stack_id") + user_id = data.get("grafana_user_id") + + organization = Organization.objects.get(stack_id=stack_id) + user = organization.users.get(user_id=user_id) + try: + uninstall_slack_integration(organization, user) + except SlackInstallationExc as e: + logger.exception( + f'msg="SlackInstallationHandler: Failed to uninstall Slack integration: %s" org_id={organization.id} stack_id={stack_id}', + e, + ) diff --git a/engine/apps/chatops_proxy/events/root_handler.py b/engine/apps/chatops_proxy/events/root_handler.py index d9a8c6f3..4e8af9c9 100644 --- a/engine/apps/chatops_proxy/events/root_handler.py +++ b/engine/apps/chatops_proxy/events/root_handler.py @@ -1,7 +1,7 @@ import logging import typing -from .handlers import Handler, SlackInstallationHandler +from .handlers import Handler, SlackInstallHandler, SlackUninstallHandler from .types import Event logger = logging.getLogger(__name__) @@ -12,7 +12,7 @@ class ChatopsEventsHandler: ChatopsEventsHandler is a root handler which receives event from Chatops-Proxy and chooses the handler to process it. """ - HANDLERS: typing.List[typing.Type[Handler]] = [SlackInstallationHandler] + HANDLERS: typing.List[typing.Type[Handler]] = [SlackInstallHandler, SlackUninstallHandler] def handle(self, event_data: Event) -> bool: """ diff --git a/engine/apps/chatops_proxy/events/types.py b/engine/apps/chatops_proxy/events/types.py index 22ce2310..93843970 100644 --- a/engine/apps/chatops_proxy/events/types.py +++ b/engine/apps/chatops_proxy/events/types.py @@ -15,3 +15,9 @@ class IntegrationInstalledData(typing.TypedDict): stack_id: int grafana_user_id: int payload: dict + + +class IntegrationUninstalledData(typing.TypedDict): + provider_type: str + stack_id: int + grafana_user_id: int diff --git a/engine/apps/chatops_proxy/tests/test_events.py b/engine/apps/chatops_proxy/tests/test_events.py index 01891bdb..79bb2700 100644 --- a/engine/apps/chatops_proxy/tests/test_events.py +++ b/engine/apps/chatops_proxy/tests/test_events.py @@ -4,10 +4,10 @@ import pytest from django.test import override_settings from apps.chatops_proxy.events import ChatopsEventsHandler -from apps.chatops_proxy.events.handlers import SlackInstallationHandler +from apps.chatops_proxy.events.handlers import SlackInstallHandler, SlackUninstallHandler from common.constants.slack_auth import SLACK_OAUTH_ACCESS_RESPONSE -installation_event = { +install_event = { "event_type": "integration_installed", "data": { "provider_type": "slack", @@ -17,6 +17,15 @@ installation_event = { }, } +uninstall_event = { + "event_type": "integration_uninstalled", + "data": { + "provider_type": "slack", + "stack_id": "stack_id", + "grafana_user_id": "grafana_user_id", + }, +} + unknown_event = { "event_type": "unknown_event", "data": { @@ -37,7 +46,8 @@ invalid_schema_event = { @pytest.mark.parametrize( "payload,is_handled", [ - (installation_event, True), + (install_event, True), + (uninstall_event, True), (unknown_event, False), (invalid_schema_event, False), ], @@ -54,13 +64,30 @@ def test_root_event_handler(mock_exec, payload, is_handled): def test_slack_installation_handler(mock_install_slack_integration, make_organization_and_user): organization, user = make_organization_and_user() - installation_event["data"].update({"stack_id": organization.stack_id, "grafana_user_id": user.user_id}) + install_event["data"].update({"stack_id": organization.stack_id, "grafana_user_id": user.user_id}) - h = SlackInstallationHandler() + h = SlackInstallHandler() assert h.match(unknown_event) is False assert h.match(invalid_schema_event) is False - assert h.match(installation_event) is True - h.handle(installation_event["data"]) - assert mock_install_slack_integration.call_args.args == (organization, user, installation_event["data"]["payload"]) + assert h.match(install_event) is True + h.handle(install_event["data"]) + assert mock_install_slack_integration.call_args.args == (organization, user, install_event["data"]["payload"]) + + +@patch("apps.chatops_proxy.events.handlers.uninstall_slack_integration", return_value=None) +@pytest.mark.django_db +def test_slack_uninstall_handler(mock_uninstall_slack_integration, make_organization_and_user): + organization, user = make_organization_and_user() + + uninstall_event["data"].update({"stack_id": organization.stack_id, "grafana_user_id": user.user_id}) + + h = SlackUninstallHandler() + + assert h.match(unknown_event) is False + assert h.match(invalid_schema_event) is False + + assert h.match(uninstall_event) is True + h.handle(uninstall_event["data"]) + assert mock_uninstall_slack_integration.call_args.args == (organization, user) From 13f8c254fab430c4b9b32b4847214a0db81f4963 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 14 Jun 2024 12:26:41 -0400 Subject: [PATCH 04/19] update product areas in issue templates --- .github/ISSUE_TEMPLATE/0-bug-report-template.yml | 6 ++++-- .github/ISSUE_TEMPLATE/1-feature-request-template.yml | 6 ++++-- .github/workflows/on-issue-creation.yml | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/0-bug-report-template.yml b/.github/ISSUE_TEMPLATE/0-bug-report-template.yml index 9cf697cc..4c45fda0 100644 --- a/.github/ISSUE_TEMPLATE/0-bug-report-template.yml +++ b/.github/ISSUE_TEMPLATE/0-bug-report-template.yml @@ -70,10 +70,12 @@ body: - Alert Flow & Configuration - Auth - Chatops - - Helm - Mobile App - Schedules - - Terraform + - Helm + - API + - Terraform/Crossplane + - Metrics - Other validations: required: true diff --git a/.github/ISSUE_TEMPLATE/1-feature-request-template.yml b/.github/ISSUE_TEMPLATE/1-feature-request-template.yml index c33f3009..49c06e70 100644 --- a/.github/ISSUE_TEMPLATE/1-feature-request-template.yml +++ b/.github/ISSUE_TEMPLATE/1-feature-request-template.yml @@ -35,10 +35,12 @@ body: - Alert Flow & Configuration - Auth - Chatops - - Helm - Mobile App - Schedules - - Terraform + - Helm + - API + - Terraform/Crossplane + - Metrics - Other validations: required: true diff --git a/.github/workflows/on-issue-creation.yml b/.github/workflows/on-issue-creation.yml index b1040e49..475f066e 100644 --- a/.github/workflows/on-issue-creation.yml +++ b/.github/workflows/on-issue-creation.yml @@ -82,6 +82,8 @@ jobs: ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Helm') && 'part:deployment/helm' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Mobile App') && 'part:mobile' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Schedules') && 'part:schedules' || '' }} - ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Terraform') && 'part:API/Terraform' || '' }} + ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'API') && 'part:API' || '' }} + ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Terraform/Crossplane') && 'part:Terraform/Crossplane' || '' }} + ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Metrics') && 'part:metrics/logging' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Other') && 'more info needed' || '' }} # yamllint enable rule:line-length From a9b49a94785a47843f1269d64c5e9ebf9c44c322 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 14 Jun 2024 12:31:36 -0400 Subject: [PATCH 05/19] update api docs (#4539) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What this PR does opened in favour of https://github.com/grafana/oncall/pull/4252 --------- Co-authored-by: Jara Suárez de Puga García --- docs/sources/oncall-api-reference/_index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/oncall-api-reference/_index.md b/docs/sources/oncall-api-reference/_index.md index b3df820a..13852be1 100644 --- a/docs/sources/oncall-api-reference/_index.md +++ b/docs/sources/oncall-api-reference/_index.md @@ -37,6 +37,8 @@ Grafana OnCall uses API keys to allow access to the API. You can request a new O An API key is specific to a user and a Grafana stack. If you want to switch to a different stack configuration, request a different API key. +The endpoint refers to the OnCall Application endpoint and can be found on the OnCall -> Settings page as well. + ## Pagination List endpoints such as List Integrations or List Alert Groups return multiple objects. From 6bb9fcef4cc7b697b0f717c6672a298dda0ae708 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 14 Jun 2024 12:52:14 -0400 Subject: [PATCH 06/19] update github issue product areas + label assignment --- .github/ISSUE_TEMPLATE/0-bug-report-template.yml | 5 +++-- .github/ISSUE_TEMPLATE/1-feature-request-template.yml | 5 +++-- .github/workflows/on-issue-creation.yml | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/0-bug-report-template.yml b/.github/ISSUE_TEMPLATE/0-bug-report-template.yml index 4c45fda0..9bfb1c41 100644 --- a/.github/ISSUE_TEMPLATE/0-bug-report-template.yml +++ b/.github/ISSUE_TEMPLATE/0-bug-report-template.yml @@ -72,10 +72,11 @@ body: - Chatops - Mobile App - Schedules - - Helm - API - - Terraform/Crossplane - Metrics + - Terraform/Crossplane + - Helm/Kubernetes/Docker + - CI/CD - Other validations: required: true diff --git a/.github/ISSUE_TEMPLATE/1-feature-request-template.yml b/.github/ISSUE_TEMPLATE/1-feature-request-template.yml index 49c06e70..011a8e4f 100644 --- a/.github/ISSUE_TEMPLATE/1-feature-request-template.yml +++ b/.github/ISSUE_TEMPLATE/1-feature-request-template.yml @@ -37,10 +37,11 @@ body: - Chatops - Mobile App - Schedules - - Helm - API - - Terraform/Crossplane - Metrics + - Terraform/Crossplane + - Helm/Kubernetes/Docker + - CI/CD - Other validations: required: true diff --git a/.github/workflows/on-issue-creation.yml b/.github/workflows/on-issue-creation.yml index 475f066e..23307322 100644 --- a/.github/workflows/on-issue-creation.yml +++ b/.github/workflows/on-issue-creation.yml @@ -79,11 +79,12 @@ jobs: ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Alert Flow & Configuration') && 'part:alert flow & configuration' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Auth') && 'part:auth/teams' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Chatops') && 'part:chatops' || '' }} - ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Helm') && 'part:deployment/helm' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Mobile App') && 'part:mobile' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Schedules') && 'part:schedules' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'API') && 'part:API' || '' }} - ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Terraform/Crossplane') && 'part:Terraform/Crossplane' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Metrics') && 'part:metrics/logging' || '' }} + ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Terraform/Crossplane') && 'part:Terraform/Crossplane' || '' }} + ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Helm/Kubernetes/Docker') && 'part:helm/kubernetes/docker' || '' }} + ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'CI/CD') && 'part:ci/cd' || '' }} ${{ contains(steps.issue-form-values.outputs.issueparser_product_area, 'Other') && 'more info needed' || '' }} # yamllint enable rule:line-length From 8f64a44e54ca3baf84378c17b63b4ab3cd658cd2 Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Mon, 17 Jun 2024 09:47:56 +0200 Subject: [PATCH 07/19] Fix "Cannot read ... of undefined" from Axios response (#4541) # What this PR does Fix "Cannot read ... of undefined" from Axios response ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- grafana-plugin/src/utils/faro.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/grafana-plugin/src/utils/faro.ts b/grafana-plugin/src/utils/faro.ts index c9212e95..782511aa 100644 --- a/grafana-plugin/src/utils/faro.ts +++ b/grafana-plugin/src/utils/faro.ts @@ -105,19 +105,19 @@ class BaseFaroHelper { this.faro?.api.pushEvent(name, { url: res?.config?.url, status: `${res?.status}`, - statusText: `${res.statusText}`, - method: res.config?.method.toUpperCase(), + statusText: `${res?.statusText}`, + method: res?.config?.method.toUpperCase(), }); }; - pushAxiosNetworkError = (res: AxiosResponse) => { - this.faro?.api.pushError(new Error(`Network error: ${res.status}`), { + pushAxiosNetworkError = (res?: AxiosResponse) => { + this.faro?.api.pushError(new Error(`Network error: ${res?.status}`), { context: { - url: res.config?.url, + url: res?.config?.url, type: 'network', data: `${safeJSONStringify(res.data)}`, - status: `${res.status}`, - statusText: `${res.statusText}`, + status: `${res?.status}`, + statusText: `${res?.statusText}`, timestamp: new Date().toUTCString(), }, }); From b7dbb2a26e517fa97a3ae2caafb96cd2969f2778 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Mon, 17 Jun 2024 11:31:43 +0100 Subject: [PATCH 08/19] Support message shortcut broadcast (#4518) # What this PR does Related to https://github.com/grafana/oncall-gateway/issues/206 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- .../apps/slack/scenarios/resolution_note.py | 29 ++++- .../test_resolution_note.py | 108 ++++++++++++++++++ 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/engine/apps/slack/scenarios/resolution_note.py b/engine/apps/slack/scenarios/resolution_note.py index a5bfb279..67ddea66 100644 --- a/engine/apps/slack/scenarios/resolution_note.py +++ b/engine/apps/slack/scenarios/resolution_note.py @@ -3,6 +3,7 @@ import json import logging import typing +from django.conf import settings from django.db.models import Q from django.utils.text import Truncator @@ -76,16 +77,28 @@ class AddToResolutionNoteStep(scenario_step.ScenarioStep): warning_text = "Unable to add this message to resolution note, this command works only in incident threads." + # thread_ts is only present for thread messages + thread_ts = payload.get("message", {}).get("thread_ts") + if not thread_ts: + if settings.UNIFIED_SLACK_APP_ENABLED: + # Message shortcut events are broadcasted to multiple regions by chatops-proxy + # Do not open a warning window to avoid multiple regions opening the same window multiple times + return + + self.open_warning_window(payload, warning_text) + return + try: slack_message = SlackMessage.objects.get( slack_id=payload["message"]["thread_ts"], _slack_team_identity=slack_team_identity, channel_id=channel_id, ) - except KeyError: - self.open_warning_window(payload, warning_text) - return except SlackMessage.DoesNotExist: + if settings.UNIFIED_SLACK_APP_ENABLED: + # Message shortcut events are broadcasted to multiple regions by chatops-proxy + # Don't open a warning window as this event could be handled by another region + return self.open_warning_window(payload, warning_text) return @@ -99,9 +112,17 @@ class AddToResolutionNoteStep(scenario_step.ScenarioStep): ) return + if alert_group.channel.organization.deleted_at is not None: + if settings.UNIFIED_SLACK_APP_ENABLED: + # Message shortcut events are broadcasted to multiple regions by chatops-proxy + # Don't open a warning window as this event could be handled by another region + return + + self.open_warning_window(payload, warning_text) + return + if payload["message"]["type"] == "message" and "user" in payload["message"]: message_ts = payload["message_ts"] - thread_ts = payload["message"]["thread_ts"] result = self._slack_client.chat_getPermalink(channel=channel_id, message_ts=message_ts) permalink = None diff --git a/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py b/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py index 5ed39ecf..e3e9cb02 100644 --- a/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py +++ b/engine/apps/slack/tests/test_scenario_steps/test_resolution_note.py @@ -334,3 +334,111 @@ def test_resolution_notes_modal_closed_before_update( # Check that "views.update" API call was made call_args, _ = mock_slack_api_call.call_args assert call_args[0] == "views.update" + + +@patch.object(SlackClient, "chat_getPermalink", return_value={"permalink": "https://example.com"}) +@pytest.mark.django_db +def test_add_to_resolution_note( + _, + make_organization_and_user_with_slack_identities, + make_alert_receive_channel, + make_alert_group, + make_alert, + make_slack_message, + settings, +): + organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + make_alert(alert_group=alert_group, raw_request_data={}) + slack_message = make_slack_message(alert_group=alert_group) + + payload = { + "channel": {"id": slack_message.channel_id}, + "message_ts": "random_ts", + "message": { + "type": "message", + "text": "Test resolution note", + "ts": "random_ts", + "thread_ts": slack_message.slack_id, + "user": slack_user_identity.slack_id, + }, + "trigger_id": "random_trigger_id", + } + + AddToResolutionNoteStep = ScenarioStep.get_step("resolution_note", "AddToResolutionNoteStep") + step = AddToResolutionNoteStep(organization=organization, user=user, slack_team_identity=slack_team_identity) + with patch.object(SlackClient, "reactions_add") as mock_reactions_add: + step.process_scenario(slack_user_identity, slack_team_identity, payload) + + mock_reactions_add.assert_called_once() + assert alert_group.resolution_notes.get().text == "Test resolution note" + + +@pytest.mark.django_db +def test_add_to_resolution_note_broadcast(make_organization_and_user_with_slack_identities, settings): + settings.UNIFIED_SLACK_APP_ENABLED = True + + organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() + + payload = { + "channel": {"id": "TEST"}, + "message_ts": "TEST", + "message": {"thread_ts": "TEST"}, + "trigger_id": "TEST", + } + + AddToResolutionNoteStep = ScenarioStep.get_step("resolution_note", "AddToResolutionNoteStep") + step = AddToResolutionNoteStep(organization=organization, user=user, slack_team_identity=slack_team_identity) + with patch.object(SlackClient, "api_call") as mock_api_call: + step.process_scenario(slack_user_identity, slack_team_identity, payload) + + mock_api_call.assert_not_called() # no Slack API calls should be made + + +@patch.object(SlackClient, "chat_getPermalink", return_value={"permalink": "https://example.com"}) +@pytest.mark.django_db +def test_add_to_resolution_note_deleted_org( + _, + make_organization_and_user_with_slack_identities, + make_alert_receive_channel, + make_alert_group, + make_alert, + make_slack_message, + make_organization, + make_user_for_organization, + settings, +): + settings.UNIFIED_SLACK_APP_ENABLED = True + + organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities() + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + make_alert(alert_group=alert_group, raw_request_data={}) + slack_message = make_slack_message(alert_group=alert_group) + organization.delete() + + other_organization = make_organization(slack_team_identity=slack_team_identity) + other_user = make_user_for_organization(organization=other_organization, slack_user_identity=slack_user_identity) + + payload = { + "channel": {"id": slack_message.channel_id}, + "message_ts": "random_ts", + "message": { + "type": "message", + "text": "Test resolution note", + "ts": "random_ts", + "thread_ts": slack_message.slack_id, + "user": slack_user_identity.slack_id, + }, + "trigger_id": "random_trigger_id", + } + + AddToResolutionNoteStep = ScenarioStep.get_step("resolution_note", "AddToResolutionNoteStep") + step = AddToResolutionNoteStep( + organization=other_organization, user=other_user, slack_team_identity=slack_team_identity + ) + with patch.object(SlackClient, "api_call") as mock_api_call: + step.process_scenario(slack_user_identity, slack_team_identity, payload) + + mock_api_call.assert_not_called() # no Slack API calls should be made From 46629e2ea63f6004aa43c870db2b4c9f9a22e5c9 Mon Sep 17 00:00:00 2001 From: Rares Mardare Date: Mon, 17 Jun 2024 16:30:08 +0300 Subject: [PATCH 09/19] Make rotation start and rotation end take timezone into consideration (#4481) # What this PR does - Fixes Rotation Start and Rotation End to take selected offset into consideration - Fixed issue where week/month period was being restored when the offset was being changed (now it defaults to start of week, midnight, in selected timezone offset) Related to https://github.com/grafana/oncall/issues/4428 --- .../src/containers/Rotation/Rotation.tsx | 4 +- .../RotationForm/RotationForm.helpers.test.ts | 21 +--- .../RotationForm/RotationForm.helpers.ts | 34 +----- .../containers/RotationForm/RotationForm.tsx | 103 ++++++++++++------ .../RotationForm/ScheduleOverrideForm.tsx | 2 +- .../containers/RotationForm/ShiftSwapForm.tsx | 2 +- .../RotationForm/parts/DateTimePicker.tsx | 55 +++++----- .../containers/Rotations/Rotations.helpers.ts | 32 ++++++ .../src/containers/Rotations/Rotations.tsx | 33 ++++-- .../containers/Rotations/ScheduleFinal.tsx | 4 +- .../containers/Rotations/SchedulePersonal.tsx | 4 +- .../src/models/timezone/timezone.ts | 17 ++- .../src/pages/schedule/Schedule.helpers.ts | 35 +++++- .../src/pages/schedule/Schedule.tsx | 26 +++-- 14 files changed, 221 insertions(+), 151 deletions(-) diff --git a/grafana-plugin/src/containers/Rotation/Rotation.tsx b/grafana-plugin/src/containers/Rotation/Rotation.tsx index 6f56d6a0..4210eaf1 100644 --- a/grafana-plugin/src/containers/Rotation/Rotation.tsx +++ b/grafana-plugin/src/containers/Rotation/Rotation.tsx @@ -40,7 +40,7 @@ interface RotationProps { export const Rotation: FC = observer((props) => { const { - timezoneStore: { calendarStartDate, getDateInSelectedTimezone }, + timezoneStore: { calendarStartDate, getDateInSelectedTimezone, selectedTimezoneOffset }, scheduleStore: { scheduleView: storeScheduleView }, } = useStore(); const { @@ -144,7 +144,7 @@ export const Rotation: FC = observer((props) => { const base = 60 * 60 * 24 * days; return firstShiftOffset / base; - }, [events, startDate]); + }, [events, startDate, selectedTimezoneOffset]); return (
diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.helpers.test.ts b/grafana-plugin/src/containers/RotationForm/RotationForm.helpers.test.ts index 6814710f..d36e72c2 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.helpers.test.ts +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.helpers.test.ts @@ -2,31 +2,12 @@ import dayjs, { Dayjs } from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; -import { dayJSAddWithDSTFixed, getDateForDatePicker } from './RotationForm.helpers'; +import { dayJSAddWithDSTFixed } from './RotationForm.helpers'; dayjs.extend(timezone); dayjs.extend(utc); describe('RotationForm helpers', () => { - describe('getDateForDatePicker()', () => { - it(`should return the same regular JS Date as input dayJsDate - even if selected day of month doesn't exist in current month - (in this case there is no 30th Feb and it should still work ok)`, () => { - jest.useFakeTimers().setSystemTime(new Date('2024-02-01')); - - const inputDate = dayjs() - .utcOffset(360) - .set('year', 2024) - .set('month', 3) // 0-indexed so April - .set('date', 30) - .set('hour', 12) - .set('minute', 20); - const result = getDateForDatePicker(inputDate); - - expect(result.toString()).toContain('Tue Apr 30 2024'); - }); - }); - describe('dayJSAddWithDSTFixed() @london-tz', () => { it(`corrects resulting hour to be the same as in input if start date is before London DST (GMT + 0) and resulting date is within London DST (GMT + 1)`, () => { diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.helpers.ts b/grafana-plugin/src/containers/RotationForm/RotationForm.helpers.ts index b0651d16..da86c3b5 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.helpers.ts +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.helpers.ts @@ -1,6 +1,4 @@ -import dayjs, { Dayjs, ManipulateType } from 'dayjs'; - -import { Timezone } from 'models/timezone/timezone.types'; +import { Dayjs, ManipulateType } from 'dayjs'; import { RepeatEveryPeriod } from './RotationForm.types'; @@ -11,19 +9,6 @@ export const getRepeatShiftsEveryOptions = (repeatEveryPeriod: number) => { .map((i) => ({ label: String(i), value: i })); }; -export const toDate = (moment: dayjs.Dayjs, timezone: Timezone) => { - const localMoment = moment.tz(timezone); - - return new Date( - localMoment.get('year'), - localMoment.get('month'), - localMoment.get('date'), - localMoment.get('hour'), - localMoment.get('minute'), - localMoment.get('second') - ); -}; - export interface TimeUnit { unit: RepeatEveryPeriod; value: number; @@ -171,23 +156,6 @@ export const repeatEveryInSeconds = (repeatEveryPeriod: RepeatEveryPeriod, repea return repeatEveryPeriodMultiplier[repeatEveryPeriod] * repeatEveryValue; }; -export const getDateForDatePicker = (dayJsDate: Dayjs) => { - const date = new Date(); - // Day of the month needs to be set to 1st day at first to prevent incorrect month increment - // when selected day of month doesn't exist in current month - // E.g. selected date is 30th March and current month is Feb, so in this case date.setMonth(2) results in April - - date.setDate(1); // temporary selection to prevent incorrect month increment - - date.setFullYear(dayJsDate.year()); - date.setMonth(dayJsDate.month()); - date.setDate(dayJsDate.date()); - date.setHours(dayJsDate.hour()); - date.setMinutes(dayJsDate.minute()); - date.setSeconds(dayJsDate.second()); - return date; -}; - export const dayJSAddWithDSTFixed = ({ baseDate, addParams, diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index c9c6376a..b068f68e 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { + Alert, Button, Field, HorizontalGroup, @@ -52,11 +53,11 @@ import { ApiSchemas } from 'network/oncall-api/api.types'; import { getDateTime, getSelectedDays, - getStartOfWeekBasedOnCurrentDate, getUTCByDay, getUTCString, getUTCWeekStart, getWeekStartString, + toDateWithTimezoneOffset, } from 'pages/schedule/Schedule.helpers'; import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers'; import { useStore } from 'state/useStore'; @@ -82,8 +83,25 @@ interface RotationFormProps { onShowRotationForm: (shiftId: Shift['id']) => void; } +const getStartShift = (start: dayjs.Dayjs, timezoneOffset: number, isNewRotation = false) => { + if (isNewRotation) { + // all new rotations default to midnight in selected timezone offset + return toDateWithTimezoneOffset(start, timezoneOffset) + .set('date', 1) + .set('year', start.year()) + .set('month', start.month()) + .set('date', start.date()) + .set('hour', 0) + .set('minute', 0) + .set('second', 0); + } + + return toDateWithTimezoneOffset(start, timezoneOffset); +}; + export const RotationForm = observer((props: RotationFormProps) => { const store = useStore(); + const { onHide, onCreate, @@ -92,7 +110,7 @@ export const RotationForm = observer((props: RotationFormProps) => { onDelete, layerPriority, shiftId, - shiftStart: propsShiftStart = getStartOfWeekBasedOnCurrentDate(store.timezoneStore.currentDateInSelectedTimezone), + shiftStart: propsShiftStart = store.timezoneStore.calendarStartDate, shiftEnd: propsShiftEnd, shiftColor = '#3D71D9', onShowRotationForm, @@ -101,7 +119,6 @@ export const RotationForm = observer((props: RotationFormProps) => { const shift = store.scheduleStore.shifts[shiftId]; const [errors, setErrors] = useState<{ [key: string]: string[] }>({}); - const [bounds, setDraggableBounds] = useState<{ left: number; right: number; top: number; bottom: number }>( undefined ); @@ -111,13 +128,19 @@ export const RotationForm = observer((props: RotationFormProps) => { const [offsetTop, setOffsetTop] = useState(GRAFANA_HEADER_HEIGHT + 10); const [draggablePosition, setDraggablePosition] = useState<{ x: number; y: number }>(undefined); - const [shiftStart, setShiftStart] = useState(propsShiftStart); - const [shiftEnd, setShiftEnd] = useState(propsShiftEnd || shiftStart.add(1, 'day')); + const [shiftStart, setShiftStart] = useState( + getStartShift(propsShiftStart, store.timezoneStore.selectedTimezoneOffset, shiftId === 'new') + ); + + const [shiftEnd, setShiftEnd] = useState( + propsShiftEnd?.utcOffset(store.timezoneStore.selectedTimezoneOffset) || shiftStart.add(1, 'day') + ); + const [activePeriod, setActivePeriod] = useState(undefined); const [shiftPeriodDefaultValue, setShiftPeriodDefaultValue] = useState(undefined); const [rotationStart, setRotationStart] = useState(shiftStart); - const [endLess, setEndless] = useState(true); + const [endLess, setEndless] = useState(shift?.until === undefined ? true : !Boolean(shift.until)); const [rotationEnd, setRotationEnd] = useState(shiftStart.add(1, 'month')); const [repeatEveryValue, setRepeatEveryValue] = useState(1); @@ -248,10 +271,11 @@ export const RotationForm = observer((props: RotationFormProps) => { shift, endLess, rotationName, + store.timezoneStore.selectedTimezoneOffset, ] ); - useEffect(handleChange, [params, store.timezoneStore.calendarStartDate]); + useEffect(handleChange, [params, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]); const create = useCallback(async () => { try { @@ -330,28 +354,22 @@ export const RotationForm = observer((props: RotationFormProps) => { } }; - const handleRotationStartChange = useCallback( - (value) => { - setRotationStart(value); - setShiftStart(value); - if (showActiveOnSelectedPartOfDay) { - setShiftEnd( - dayJSAddWithDSTFixed({ + const handleRotationStartChange = (value: dayjs.Dayjs) => { + setRotationStart(value); + setShiftStart(value); + + setShiftEnd( + showActiveOnSelectedPartOfDay + ? dayJSAddWithDSTFixed({ baseDate: value, addParams: [activePeriod, 'seconds'], }) - ); - } else { - setShiftEnd( - dayJSAddWithDSTFixed({ + : dayJSAddWithDSTFixed({ baseDate: value, addParams: [repeatEveryValue, repeatEveryPeriodToUnitName[repeatEveryPeriod]], }) - ); - } - }, - [showActiveOnSelectedPartOfDay, activePeriod, repeatEveryPeriod, repeatEveryValue] - ); + ); + }; const handleActivePeriodChange = useCallback( (value) => { @@ -435,15 +453,23 @@ export const RotationForm = observer((props: RotationFormProps) => { useEffect(() => { if (shift) { setRotationName(getShiftName(shift)); - const shiftStart = getDateTime(shift.shift_start); + // use shiftStart as rotationStart for existing shifts // (original rotationStart defaulted to the shift creation timestamp) + const shiftStart = toDateWithTimezoneOffset(dayjs(shift.shift_start), store.timezoneStore.selectedTimezoneOffset); + setRotationStart(shiftStart); - setRotationEnd(shift.until ? getDateTime(shift.until) : getDateTime(shift.shift_start).add(1, 'month')); + setRotationEnd( + toDateWithTimezoneOffset( + // always keep the date offseted + shift.until ? getDateTime(shift.until) : getDateTime(shift.shift_start).add(1, 'month'), + store.timezoneStore.selectedTimezoneOffset + ) + ); setShiftStart(shiftStart); - const shiftEnd = getDateTime(shift.shift_end); + + const shiftEnd = toDateWithTimezoneOffset(dayjs(shift.shift_end), store.timezoneStore.selectedTimezoneOffset); setShiftEnd(shiftEnd); - setEndless(!shift.until); setRepeatEveryValue(shift.interval); setRepeatEveryPeriod(shift.frequency); @@ -475,6 +501,10 @@ export const RotationForm = observer((props: RotationFormProps) => { useEffect(() => { if (shift) { + // for existing rotations + handleRotationStartChange(toDateWithTimezoneOffset(rotationStart, store.timezoneStore.selectedTimezoneOffset)); + setRotationEnd(toDateWithTimezoneOffset(rotationEnd, store.timezoneStore.selectedTimezoneOffset)); + setSelectedDays( getSelectedDays({ dayOptions: store.scheduleStore.byDayOptions, @@ -482,6 +512,14 @@ export const RotationForm = observer((props: RotationFormProps) => { moment: store.timezoneStore.getDateInSelectedTimezone(shiftStart), }) ); + } else { + // for new rotations + handleRotationStartChange(toDateWithTimezoneOffset(rotationStart, store.timezoneStore.selectedTimezoneOffset)); + + setShiftEnd(toDateWithTimezoneOffset(shiftEnd, store.timezoneStore.selectedTimezoneOffset)); + + // not behind an "if" such that it will reflect correct value after toggle gets switched + setRotationEnd(toDateWithTimezoneOffset(rotationEnd, store.timezoneStore.selectedTimezoneOffset)); } }, [store.timezoneStore.selectedTimezoneOffset]); @@ -561,14 +599,11 @@ export const RotationForm = observer((props: RotationFormProps) => { )} {!hasUpdatedShift && ended && ( - +
- - - This rotation is over - + This rotation is over) as unknown as string} /> - +
)}
{ > { ) : ( = (props) => { const handleChange = useDebouncedCallback(updatePreview, 200); - useEffect(handleChange, [params, store.timezoneStore.calendarStartDate]); + useEffect(handleChange, [params, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]); const isFormValid = useMemo(() => !Object.keys(errors).length, [errors]); diff --git a/grafana-plugin/src/containers/RotationForm/ShiftSwapForm.tsx b/grafana-plugin/src/containers/RotationForm/ShiftSwapForm.tsx index 7795b4dd..dee6d5f6 100644 --- a/grafana-plugin/src/containers/RotationForm/ShiftSwapForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/ShiftSwapForm.tsx @@ -87,7 +87,7 @@ export const ShiftSwapForm = (props: ShiftSwapFormProps) => { ...shiftSwap, }); } - }, [shiftSwap, store.timezoneStore.calendarStartDate]); + }, [shiftSwap, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]); const handleDescriptionChange = useCallback( (event) => { diff --git a/grafana-plugin/src/containers/RotationForm/parts/DateTimePicker.tsx b/grafana-plugin/src/containers/RotationForm/parts/DateTimePicker.tsx index 6dc88a26..3c90b836 100644 --- a/grafana-plugin/src/containers/RotationForm/parts/DateTimePicker.tsx +++ b/grafana-plugin/src/containers/RotationForm/parts/DateTimePicker.tsx @@ -8,8 +8,8 @@ import dayjs from 'dayjs'; import { observer } from 'mobx-react'; import { Text } from 'components/Text/Text'; -import { getDateForDatePicker } from 'containers/RotationForm/RotationForm.helpers'; -import { useStore } from 'state/useStore'; +import { toDatePickerDate } from 'containers/Rotations/Rotations.helpers'; +import { toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers'; import styles from 'containers/RotationForm/RotationForm.module.css'; @@ -17,6 +17,7 @@ const cx = cn.bind(styles); interface DateTimePickerProps { value: dayjs.Dayjs; + utcOffset?: number; onChange: (value: dayjs.Dayjs) => void; disabled?: boolean; onFocus?: () => void; @@ -25,39 +26,37 @@ interface DateTimePickerProps { } export const DateTimePicker = observer( - ({ value: propValue, onChange, disabled, onFocus, onBlur, error }: DateTimePickerProps) => { + ({ value: propValue, utcOffset, onChange, disabled, onFocus, onBlur, error }: DateTimePickerProps) => { const styles = useStyles2(getStyles); - const { - timezoneStore: { getDateInSelectedTimezone }, - } = useStore(); - const valueInSelectedTimezone = getDateInSelectedTimezone(propValue); - const valueAsDate = valueInSelectedTimezone.toDate(); - const handleDateChange = (newDate: Date) => { - const localMoment = getDateInSelectedTimezone(dayjs(newDate)); - const newValue = localMoment - .set('year', newDate.getFullYear()) - .set('month', newDate.getMonth()) - .set('date', newDate.getDate()) - .set('hour', valueAsDate.getHours()) - .set('minute', valueAsDate.getMinutes()) - .set('second', valueAsDate.getSeconds()); + const handleDateChange = (value: Date) => { + const newDate = toDateWithTimezoneOffset(dayjs(value), utcOffset) + .set('date', 1) + .set('months', value.getMonth()) + .set('date', value.getDate()) + .set('hours', propValue.hour()) + .set('minutes', propValue.minute()) + .set('second', 0) + .set('milliseconds', 0); - onChange(newValue); + onChange(newDate); }; - const handleTimeChange = (newMoment: DateTime) => { - const selectedHour = newMoment.hour(); - const selectedMinute = newMoment.minute(); - const newValue = valueInSelectedTimezone.set('hour', selectedHour).set('minute', selectedMinute); - onChange(newValue); + const handleTimeChange = (timeMoment: DateTime) => { + const newDate = toDateWithTimezoneOffset(propValue, utcOffset) + .set('hour', timeMoment.hour()) + .set('minute', timeMoment.minute()); + + onChange(newDate); }; const getTimeValueInSelectedTimezone = () => { - const time = dateTime(valueInSelectedTimezone.format()); - time.set('hour', valueInSelectedTimezone.hour()); - time.set('minute', valueInSelectedTimezone.minute()); - time.set('second', valueInSelectedTimezone.second()); + const dateInOffset = toDateWithTimezoneOffset(propValue, utcOffset); + + const time = dateTime(dateInOffset.format()); + time.set('hour', dateInOffset.hour()); + time.set('minute', dateInOffset.minute()); + time.set('seconds', dateInOffset.second()); return time; }; @@ -73,7 +72,7 @@ export const DateTimePicker = observer(
diff --git a/grafana-plugin/src/containers/Rotations/Rotations.helpers.ts b/grafana-plugin/src/containers/Rotations/Rotations.helpers.ts index 9d2a47d2..ac3bc857 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.helpers.ts +++ b/grafana-plugin/src/containers/Rotations/Rotations.helpers.ts @@ -3,6 +3,38 @@ import dayjs from 'dayjs'; import { getColor, getOverrideColor } from 'models/schedule/schedule.helpers'; import { Layer, Shift } from 'models/schedule/schedule.types'; import { ApiSchemas } from 'network/oncall-api/api.types'; +import { toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers'; + +// DatePickers will convert the date passed to local timezone, instead we want to use the date in the given timezone +export const toDatePickerDate = (value: dayjs.Dayjs, timezoneOffset: number) => { + const date = toDateWithTimezoneOffset(value, timezoneOffset); + + return dayjs() + .set('hour', 0) + .set('minute', 0) + .set('second', 0) + .set('millisecond', 0) + .set('date', 1) + .set('month', date.month()) + .set('date', date.date()) + .set('year', date.year()) + .toDate(); +}; + +export const getCalendarStartDateInTimezone = (calendarStartDate: dayjs.Dayjs, utcOffset: number) => { + const offsetedDate = dayjs(calendarStartDate.toDate()) + .utcOffset(utcOffset) + .set('date', 1) + .set('months', calendarStartDate.month()) + .set('date', calendarStartDate.date()) + .set('year', calendarStartDate.year()) + .set('hours', 0) + .set('minutes', 0) + .set('second', 0) + .set('milliseconds', 0); + + return offsetedDate; +}; export const findColor = (shiftId: Shift['id'], layers: Layer[], overrides?) => { let color = undefined; diff --git a/grafana-plugin/src/containers/Rotations/Rotations.tsx b/grafana-plugin/src/containers/Rotations/Rotations.tsx index 5ba47b92..81383ffa 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.tsx +++ b/grafana-plugin/src/containers/Rotations/Rotations.tsx @@ -17,14 +17,14 @@ import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/W import { getColor, getLayersFromStore, scheduleViewToDaysInOneRow } from 'models/schedule/schedule.helpers'; import { Schedule, ScheduleType, Shift, ShiftSwap, Event, Layer } from 'models/schedule/schedule.types'; import { ApiSchemas } from 'network/oncall-api/api.types'; -import { getCurrentTimeX } from 'pages/schedule/Schedule.helpers'; +import { getCurrentTimeX, toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers'; import { WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import { HTML_ID } from 'utils/DOM'; import { UserActions } from 'utils/authorization/authorization'; import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config'; -import { findColor } from './Rotations.helpers'; +import { findColor, getCalendarStartDateInTimezone } from './Rotations.helpers'; import { getRotationsStyles } from './Rotations.styles'; import animationStyles from './Rotations.module.css'; @@ -77,6 +77,8 @@ class _Rotations extends Component { const { shiftStartToShowRotationForm, shiftEndToShowRotationForm } = this.state; + const { selectedTimezoneOffset } = store.timezoneStore; + const currentTimeX = getCurrentTimeX( store.timezoneStore.currentDateInSelectedTimezone, store.timezoneStore.calendarStartDate, @@ -140,7 +142,16 @@ class _Rotations extends Component { @@ -250,8 +261,8 @@ class _Rotations extends Component { shiftColor={findColor(shiftIdToShowRotationForm, layers)} scheduleId={scheduleId} layerPriority={layerPriorityToShowRotationForm} - shiftStart={shiftStartToShowRotationForm} - shiftEnd={shiftEndToShowRotationForm} + shiftStart={toDateWithTimezoneOffset(shiftStartToShowRotationForm, selectedTimezoneOffset)} + shiftEnd={toDateWithTimezoneOffset(shiftEndToShowRotationForm, selectedTimezoneOffset)} onHide={() => { this.hideRotationForm(); @@ -298,9 +309,15 @@ class _Rotations extends Component { return; } - this.setState({ shiftStartToShowRotationForm: shiftStart, shiftEndToShowRotationForm: shiftEnd }, () => { - this.onShowRotationForm('new', layerPriority); - }); + this.setState( + { + shiftStartToShowRotationForm: shiftStart, + shiftEndToShowRotationForm: shiftEnd, + }, + () => { + this.onShowRotationForm('new', layerPriority); + } + ); }; handleAddRotation = (option: SelectableValue) => { diff --git a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx index 723f217b..e169df9d 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx @@ -56,7 +56,7 @@ const _ScheduleFinal: FC = observer( scheduleView: propsScheduleView, }) => { const { - timezoneStore: { currentDateInSelectedTimezone, calendarStartDate }, + timezoneStore: { selectedTimezoneOffset, currentDateInSelectedTimezone, calendarStartDate }, scheduleStore: { scheduleView: storeScheduleView }, } = store; @@ -85,7 +85,7 @@ const _ScheduleFinal: FC = observer( }); } return rows; - }, [calendarStartDate, scheduleView]); + }, [calendarStartDate, scheduleView, currentDateInSelectedTimezone, selectedTimezoneOffset]); return (
= observer(({ userPk, onSlotC }; const handleTodayClick = () => { - timezoneStore.setCalendarStartDate(getStartOfWeekBasedOnCurrentDate(timezoneStore.currentDateInSelectedTimezone)); + // TODAY + timezoneStore.setCalendarStartDate(getStartOfWeekBasedOnCurrentDate(dayjs())); }; const handleLeftClick = () => { diff --git a/grafana-plugin/src/models/timezone/timezone.ts b/grafana-plugin/src/models/timezone/timezone.ts index e8561c79..10995b0b 100644 --- a/grafana-plugin/src/models/timezone/timezone.ts +++ b/grafana-plugin/src/models/timezone/timezone.ts @@ -3,7 +3,7 @@ import { observable, action, computed, makeObservable } from 'mobx'; // TODO: move utils from Schedule.helpers to common place import { ScheduleView } from 'models/schedule/schedule.types'; -import { getCalendarStartDate } from 'pages/schedule/Schedule.helpers'; +import { getCalendarStartDate, toDateWithTimezoneOffsetAtMidnight } from 'pages/schedule/Schedule.helpers'; import { RootStore } from 'state/rootStore'; import { getOffsetOfCurrentUser, getGMTTimezoneLabelBasedOnOffset } from './timezone.helpers'; @@ -20,19 +20,18 @@ export class TimezoneStore { @observable selectedTimezoneOffset = getOffsetOfCurrentUser(); - /* @observable - calendarStartDate = getStartOfWeekBasedOnCurrentDate(this.currentDateInSelectedTimezone); */ - @observable - calendarStartDate = getCalendarStartDate(this.currentDateInSelectedTimezone, ScheduleView.OneWeek); + calendarStartDate = getCalendarStartDate( + this.currentDateInSelectedTimezone, + ScheduleView.OneWeek, + this.selectedTimezoneOffset + ); @action.bound setSelectedTimezoneOffset(offset: number) { this.selectedTimezoneOffset = offset; - this.calendarStartDate = getCalendarStartDate( - this.currentDateInSelectedTimezone, - this.rootStore.scheduleStore.scheduleView - ); + + this.calendarStartDate = toDateWithTimezoneOffsetAtMidnight(this.calendarStartDate, offset); } @action.bound diff --git a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts index 44096057..0f2a2771 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts +++ b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts @@ -1,7 +1,7 @@ import { config } from '@grafana/runtime'; import dayjs from 'dayjs'; -import { findColor } from 'containers/Rotations/Rotations.helpers'; +import { findColor, getCalendarStartDateInTimezone } from 'containers/Rotations/Rotations.helpers'; import { getLayersFromStore, getOverridesFromStore, @@ -40,13 +40,15 @@ export const getStartOfWeekBasedOnCurrentDate = (date: dayjs.Dayjs) => { return date.startOf('isoWeek'); // it's Monday always }; -export const getCalendarStartDate = (date: dayjs.Dayjs, scheduleView: ScheduleView) => { +export const getCalendarStartDate = (date: dayjs.Dayjs, scheduleView: ScheduleView, timezoneOffset: number) => { + const offsetedDate = getCalendarStartDateInTimezone(date, timezoneOffset); + switch (scheduleView) { case ScheduleView.OneMonth: - const startOfMonth = date.startOf('month'); + const startOfMonth = offsetedDate.startOf('month'); return startOfMonth.startOf('isoWeek'); default: - return date.startOf('isoWeek'); + return offsetedDate.startOf('isoWeek'); } }; @@ -69,8 +71,8 @@ export const getCurrentTimeX = (currentDate: dayjs.Dayjs, startDate: dayjs.Dayjs return diff / baseInMinutes; }; -export const getUTCString = (moment: dayjs.Dayjs) => { - return moment.utc().format('YYYY-MM-DDTHH:mm:ss.000Z'); +export const getUTCString = (date: dayjs.Dayjs) => { + return date.utc().format('YYYY-MM-DDTHH:mm:ss.000Z'); }; export const getDateTime = (date: string) => { @@ -204,3 +206,24 @@ export const getColorSchemeMappingForUsers = ( }); } }; + +export const toDateWithTimezoneOffset = (date: dayjs.Dayjs, timezoneOffset?: number) => { + if (!date) { + return undefined; + } + if (timezoneOffset === undefined) { + return date; + } + return date.utcOffset() === timezoneOffset ? date : date.tz().utcOffset(timezoneOffset); +}; + +export const toDateWithTimezoneOffsetAtMidnight = (date: dayjs.Dayjs, timezoneOffset?: number) => { + return toDateWithTimezoneOffset(date, timezoneOffset) + .set('date', 1) + .set('year', date.year()) + .set('month', date.month()) + .set('date', date.date()) + .set('hour', 0) + .set('minute', 0) + .set('second', 0); +}; diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 80fb1bdb..3ed6623b 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -29,7 +29,7 @@ import { Text } from 'components/Text/Text'; import { WithConfirm } from 'components/WithConfirm/WithConfirm'; import { ShiftSwapForm } from 'containers/RotationForm/ShiftSwapForm'; import { Rotations } from 'containers/Rotations/Rotations'; -import { findClosestUserEvent } from 'containers/Rotations/Rotations.helpers'; +import { findClosestUserEvent, toDatePickerDate } from 'containers/Rotations/Rotations.helpers'; import { ScheduleFinal } from 'containers/Rotations/ScheduleFinal'; import { ScheduleOverrides } from 'containers/Rotations/ScheduleOverrides'; import { ScheduleForm } from 'containers/ScheduleForm/ScheduleForm'; @@ -336,10 +336,17 @@ class _SchedulePage extends React.Component { store.timezoneStore.setCalendarStartDate( - getCalendarStartDate(dayjs(newDate), scheduleView) + getCalendarStartDate( + dayjs(newDate), + scheduleView, + store.timezoneStore.selectedTimezoneOffset + ) ); this.handleDateRangeUpdate(); this.setState({ calendarStartDatePickerIsOpen: false }); @@ -362,7 +369,8 @@ class _SchedulePage extends React.Component { - const { store } = this.props; - store.timezoneStore.setCalendarStartDate( - getCalendarStartDate(store.timezoneStore.currentDateInSelectedTimezone, store.scheduleStore.scheduleView) + const { + store: { scheduleStore, timezoneStore }, + } = this.props; + + timezoneStore.setCalendarStartDate( + // TODAY + getCalendarStartDate(dayjs(), scheduleStore.scheduleView, timezoneStore.selectedTimezoneOffset) ); this.handleDateRangeUpdate(); }; From 100047c0c7ef01d6a0fdd05702504a7527a10a61 Mon Sep 17 00:00:00 2001 From: Chad Horohoe Date: Mon, 17 Jun 2024 07:14:46 -0700 Subject: [PATCH 10/19] Typofix: Grafaana -> Grafana (#4543) # What this PR does Simple doc typofix, Grafana was misspelled. ## Which issue(s) this PR closes None ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- docs/sources/set-up/get-started/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/set-up/get-started/index.md b/docs/sources/set-up/get-started/index.md index 5b6a1d41..a75a4ca3 100644 --- a/docs/sources/set-up/get-started/index.md +++ b/docs/sources/set-up/get-started/index.md @@ -46,7 +46,7 @@ OnCall is available in Grafana Cloud automatically: 3. Choose **Alerts and IRM** from the left menu 4. Click **OnCall** to access Grafana OnCall -Otherwise, to install Grafaana OnCall, refer to [Install Grafana OnCall OSS][]. +Otherwise, to install Grafana OnCall, refer to [Install Grafana OnCall OSS][]. ## How to configure Grafana OnCall From 8f3cdc3ccc80af0b0e3c1f332782733e9e8db65d Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Mon, 17 Jun 2024 17:01:33 +0200 Subject: [PATCH 11/19] Disable All option for non selectable variables (#4544) # What this PR does Disable All option for non selectable variables ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- grafana-plugin/src/pages/insights/variables.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/grafana-plugin/src/pages/insights/variables.ts b/grafana-plugin/src/pages/insights/variables.ts index b8dbd6e2..9a5cdb76 100644 --- a/grafana-plugin/src/pages/insights/variables.ts +++ b/grafana-plugin/src/pages/insights/variables.ts @@ -4,8 +4,6 @@ import { InsightsConfig } from './Insights.types'; const DEFAULT_VARIABLE_CONFIG: Partial[0]> = { hide: 0, - includeAll: true, - allValue: `.+`, isMulti: true, options: [], refresh: 1, @@ -33,6 +31,8 @@ const getVariables = ({ isOpenSource, datasource, stack }: InsightsConfig) => ({ label: 'Team', text: ['All'], value: ['$__all'], + includeAll: true, + allValue: `.+`, datasource, definition: `label_values(\${alert_groups_total}{slug=~"${stack}"},team)`, query: { @@ -47,6 +47,8 @@ const getVariables = ({ isOpenSource, datasource, stack }: InsightsConfig) => ({ label: 'Integration', text: ['All'], value: ['$__all'], + includeAll: true, + allValue: `.+`, datasource, definition: `label_values(\${alert_groups_total}{team=~"$team",slug=~"${stack}"},integration)`, query: { @@ -61,6 +63,7 @@ const getVariables = ({ isOpenSource, datasource, stack }: InsightsConfig) => ({ label: 'Service name', text: ['All'], value: ['$__all'], + includeAll: true, allValue: '($^)|(.+)', datasource, definition: `label_values(\${alert_groups_total}{slug=~"${stack}",team=~"$team"},service_name)`, @@ -85,7 +88,6 @@ const getVariables = ({ isOpenSource, datasource, stack }: InsightsConfig) => ({ value: ['oncall_alert_groups_total', 'grafanacloud_oncall_instance_alert_groups_total'], definition: 'metrics(alert_groups_total)', hide: 2, - includeAll: false, }), userNotified: new QueryVariable({ ...DEFAULT_VARIABLE_CONFIG, From c5b76a58693c05e2033001592ab3ade8f48f3727 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 17 Jun 2024 11:31:35 -0400 Subject: [PATCH 12/19] fix right-hand navigation in API docs (#4546) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What this PR does The right-hand side navigation in the OnCall API public docs ([example](https://grafana.com/docs/oncall/latest/oncall-api-reference/alertgroups/)) doesn’t render as expected vs. what we see in the Grafana API public docs ([example](https://grafana.com/docs/grafana/latest/developers/http_api/folder/); 2nd screenshot). This PR fixes that ([convo on Slack](https://raintank-corp.slack.com/archives/C045CTY1QEP/p1718388424649359)). ![Screenshot 2024-06-14 at 14 05 03](https://github.com/grafana/oncall/assets/9406895/0636d9d7-2f58-4c82-91b5-5b4af0dd3524) ![Screenshot 2024-06-14 at 14 06 36 (1)](https://github.com/grafana/oncall/assets/9406895/7a5bc2c8-ccb2-4b53-b982-d7d7596f90e2) --- .../oncall-api-reference/alertgroups.md | 16 ++++++++------- docs/sources/oncall-api-reference/alerts.md | 4 +++- .../oncall-api-reference/escalation_chains.md | 12 ++++++----- .../escalation_policies.md | 14 +++++++------ .../oncall-api-reference/integrations.md | 12 ++++++----- .../oncall-api-reference/on_call_shifts.md | 12 ++++++----- .../oncall-api-reference/outgoing_webhooks.md | 4 ++-- .../personal_notification_rules.md | 12 ++++++----- .../oncall-api-reference/resolution_notes.md | 14 +++++++------ docs/sources/oncall-api-reference/routes.md | 12 ++++++----- .../sources/oncall-api-reference/schedules.md | 20 ++++++++++--------- .../oncall-api-reference/shift_swaps.md | 16 ++++++++------- .../oncall-api-reference/slack_channels.md | 6 ++++-- .../oncall-api-reference/user_groups.md | 6 ++++-- docs/sources/oncall-api-reference/users.md | 8 +++++--- 15 files changed, 98 insertions(+), 70 deletions(-) diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index e5126fb7..353c9edc 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -4,7 +4,9 @@ title: Alert groups HTTP API weight: 400 --- -# List alert groups +# Alert groups HTTP API + +## List alert groups ```shell curl "{{API_URL}}/api/v1/alert_groups/" \ @@ -57,7 +59,7 @@ These available filter parameters should be provided as `GET` arguments: `GET {{API_URL}}/api/v1/alert_groups/` -# Alert group details +## Alert group details ```shell curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1" \ @@ -69,7 +71,7 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1" \ `GET {{API_URL}}/api/v1/alert_groups/` -# Acknowledge an alert group +## Acknowledge an alert group ```shell curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/acknowledge" \ @@ -81,7 +83,7 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/acknowledge" \ `POST {{API_URL}}/api/v1/alert_groups//acknowledge` -# Unacknowledge an alert group +## Unacknowledge an alert group ```shell curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unacknowledge" \ @@ -93,7 +95,7 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unacknowledge" \ `POST {{API_URL}}/api/v1/alert_groups//unacknowledge` -# Resolve an alert group +## Resolve an alert group ```shell curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/resolve" \ @@ -105,7 +107,7 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/resolve" \ `POST {{API_URL}}/api/v1/alert_groups//resolve` -# Unresolve an alert group +## Unresolve an alert group ```shell curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unresolve" \ @@ -117,7 +119,7 @@ curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unresolve" \ `POST {{API_URL}}/api/v1/alert_groups//unresolve` -# Delete an alert group +## Delete an alert group ```shell curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/" \ diff --git a/docs/sources/oncall-api-reference/alerts.md b/docs/sources/oncall-api-reference/alerts.md index 7d8935b7..df1c3d65 100644 --- a/docs/sources/oncall-api-reference/alerts.md +++ b/docs/sources/oncall-api-reference/alerts.md @@ -4,7 +4,9 @@ title: Alerts HTTP API weight: 100 --- -# List Alerts +# Alerts HTTP API + +## List Alerts ```shell curl "{{API_URL}}/api/v1/alerts/" \ diff --git a/docs/sources/oncall-api-reference/escalation_chains.md b/docs/sources/oncall-api-reference/escalation_chains.md index 4957402d..6c574700 100644 --- a/docs/sources/oncall-api-reference/escalation_chains.md +++ b/docs/sources/oncall-api-reference/escalation_chains.md @@ -1,10 +1,12 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_chains/ -title: Escalation Chains HTTP API +title: Escalation chains HTTP API weight: 200 --- -# Create an escalation chain +# Escalation chains HTTP API + +## Create an escalation chain ```shell curl "{{API_URL}}/api/v1/escalation_chains/" \ @@ -35,7 +37,7 @@ The above command returns JSON structured in the following way: `POST {{API_URL}}/api/v1/escalation_chains/` -# Get an escalation chain +## Get an escalation chain ```shell curl "{{API_URL}}/api/v1/escalation_chains/F5JU6KJET33FE/" \ @@ -58,7 +60,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/escalation_chains//` -# List escalation chains +## List escalation chains ```shell curl "{{API_URL}}/api/v1/escalation_chains/" \ @@ -91,7 +93,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/escalation_chains/` -# Delete an escalation chain +## Delete an escalation chain ```shell curl "{{API_URL}}/api/v1/escalation_chains/F5JU6KJET33FE/" \ diff --git a/docs/sources/oncall-api-reference/escalation_policies.md b/docs/sources/oncall-api-reference/escalation_policies.md index d574a016..3c4b419c 100644 --- a/docs/sources/oncall-api-reference/escalation_policies.md +++ b/docs/sources/oncall-api-reference/escalation_policies.md @@ -1,10 +1,12 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_policies/ -title: Escalation Policies HTTP API +title: Escalation policies HTTP API weight: 300 --- -# Create an escalation policy +# Escalation policies HTTP API + +## Create an escalation policy ```shell curl "{{API_URL}}/api/v1/escalation_policies/" \ @@ -50,7 +52,7 @@ The above command returns JSON structured in the following way: `POST {{API_URL}}/api/v1/escalation_policies/` -# Get an escalation policy +## Get an escalation policy ```shell curl "{{API_URL}}/api/v1/escalation_policies/E3GA6SJETWWJS/" \ @@ -71,7 +73,7 @@ The above command returns JSON structured in the following way: } ``` -# Update an escalation policy +## Update an escalation policy ```shell curl "{{API_URL}}/api/v1/escalation_policies/E3GA6SJETWWJS/" \ @@ -104,7 +106,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/escalation_policies//` -# List escalation policies +## List escalation policies ```shell curl "{{API_URL}}/api/v1/escalation_policies/" \ @@ -150,7 +152,7 @@ The following available filter parameter should be provided as a `GET` argument: `GET {{API_URL}}/api/v1/escalation_policies/` -# Delete an escalation policy +## Delete an escalation policy ```shell curl "{{API_URL}}/api/v1/escalation_policies/E3GA6SJETWWJS/" \ diff --git a/docs/sources/oncall-api-reference/integrations.md b/docs/sources/oncall-api-reference/integrations.md index 7f663aef..41bec8c6 100644 --- a/docs/sources/oncall-api-reference/integrations.md +++ b/docs/sources/oncall-api-reference/integrations.md @@ -4,7 +4,9 @@ title: Integrations HTTP API weight: 500 --- -# Create an integration +# Integrations HTTP API + +## Create an integration ```shell curl "{{API_URL}}/api/v1/integrations/" \ @@ -79,7 +81,7 @@ For example, to learn how to integrate Grafana OnCall with Alertmanager refer to `POST {{API_URL}}/api/v1/integrations/` -# Get integration +## Get integration ```shell curl "{{API_URL}}/api/v1/integrations/CFRPV98RPR1U8/" \ @@ -150,7 +152,7 @@ This endpoint retrieves an integration. Integrations are sources of alerts and a `GET {{API_URL}}/api/v1/integrations//` -# List integrations +## List integrations ```shell curl "{{API_URL}}/api/v1/integrations/" \ @@ -229,7 +231,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/integrations/` -# Update integration +## Update integration ```shell curl "{{API_URL}}/api/v1/integrations/CFRPV98RPR1U8/" \ @@ -302,7 +304,7 @@ The above command returns JSON structured in the following way: `PUT {{API_URL}}/api/v1/integrations//` -# Delete integration +## Delete integration Deleted integrations will stop recording new alerts from monitoring. Integration removal won't trigger removal of related alert groups or alerts. diff --git a/docs/sources/oncall-api-reference/on_call_shifts.md b/docs/sources/oncall-api-reference/on_call_shifts.md index 094b2e14..7c991d50 100644 --- a/docs/sources/oncall-api-reference/on_call_shifts.md +++ b/docs/sources/oncall-api-reference/on_call_shifts.md @@ -4,7 +4,9 @@ title: OnCall shifts HTTP API weight: 600 --- -# Create an OnCall shift +# OnCall shifts HTTP API + +## Create an OnCall shift ```shell curl "{{API_URL}}/api/v1/on_call_shifts/" \ @@ -67,7 +69,7 @@ For more information about recurrence rules, refer to [RFC 5545](https://tools.i `POST {{API_URL}}/api/v1/on_call_shifts/` -# Get OnCall shifts +## Get OnCall shifts ```shell curl "{{API_URL}}/api/v1/on_call_shifts/OH3V5FYQEYJ6M/" \ @@ -96,7 +98,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/on_call_shifts//` -# List OnCall shifts +## List OnCall shifts ```shell curl "{{API_URL}}/api/v1/on_call_shifts/" \ @@ -157,7 +159,7 @@ The following available filter parameters should be provided as `GET` arguments: `GET {{API_URL}}/api/v1/on_call_shifts/` -# Update OnCall shift +## Update OnCall shift ```shell curl "{{API_URL}}/api/v1/on_call_shifts/OH3V5FYQEYJ6M/" \ @@ -196,7 +198,7 @@ The above command returns JSON structured in the following way: `PUT {{API_URL}}/api/v1/on_call_shifts//` -# Delete OnCall shift +## Delete OnCall shift ```shell curl "{{API_URL}}/api/v1/on_call_shifts/OH3V5FYQEYJ6M/" \ diff --git a/docs/sources/oncall-api-reference/outgoing_webhooks.md b/docs/sources/oncall-api-reference/outgoing_webhooks.md index 31aaac76..bf8085d5 100644 --- a/docs/sources/oncall-api-reference/outgoing_webhooks.md +++ b/docs/sources/oncall-api-reference/outgoing_webhooks.md @@ -1,10 +1,10 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/outgoing_webhooks/ -title: Outgoing Webhooks HTTP API +title: Outgoing webhooks HTTP API weight: 700 --- -# Outgoing Webhooks +# Outgoing webhooks > ⚠️ A note about actions: Before version **v1.3.11** webhooks existed as actions within the API, the /actions > endpoint remains available and is compatible with previous callers but under the hood it will interact with the diff --git a/docs/sources/oncall-api-reference/personal_notification_rules.md b/docs/sources/oncall-api-reference/personal_notification_rules.md index 22c3d5d5..225ce57f 100644 --- a/docs/sources/oncall-api-reference/personal_notification_rules.md +++ b/docs/sources/oncall-api-reference/personal_notification_rules.md @@ -1,10 +1,12 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/personal_notification_rules/ -title: Personal Notification Rules HTTP API +title: Personal notification rules HTTP API weight: 800 --- -# Post a personal notification rule +# Personal notification rules HTTP API + +## Post a personal notification rule ```shell curl "{{API_URL}}/api/v1/personal_notification_rules/" \ @@ -41,7 +43,7 @@ The above command returns JSON structured in the following way: `POST {{API_URL}}/api/v1/personal_notification_rules/` -# Get personal notification rule +## Get personal notification rule ```shell curl "{{API_URL}}/api/v1/personal_notification_rules/ND9EHN5LN1DUU/" \ @@ -67,7 +69,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/personal_notification_rules//` -# List personal notification rules +## List personal notification rules ```shell curl "{{API_URL}}/api/v1/personal_notification_rules/" \ @@ -129,7 +131,7 @@ The following available filter parameters should be provided as `GET` arguments: `GET {{API_URL}}/api/v1/personal_notification_rules/` -# Delete a personal notification rule +## Delete a personal notification rule ```shell curl "{{API_URL}}/api/v1/personal_notification_rules/NWAL6WFJNWDD8/" \ diff --git a/docs/sources/oncall-api-reference/resolution_notes.md b/docs/sources/oncall-api-reference/resolution_notes.md index 24d4eecd..674c7d85 100644 --- a/docs/sources/oncall-api-reference/resolution_notes.md +++ b/docs/sources/oncall-api-reference/resolution_notes.md @@ -1,10 +1,12 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/resolution_notes/ -title: Resolution Notes HTTP API +title: Resolution notes HTTP API weight: 900 --- -# Create a resolution note +# Resolution notes HTTP API + +## Create a resolution note ```shell curl "{{API_URL}}/api/v1/resolution_notes/" \ @@ -39,7 +41,7 @@ The above command returns JSON structured in the following way: `POST {{API_URL}}/api/v1/resolution_notes/` -# Get a resolution note +## Get a resolution note ```shell curl "{{API_URL}}/api/v1/resolution_notes/M4BTQUS3PRHYQ/" \ @@ -65,7 +67,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/resolution_notes//` -# List resolution notes +## List resolution notes ```shell curl "{{API_URL}}/api/v1/resolution_notes/" \ @@ -105,7 +107,7 @@ The following available filter parameter should be provided as a `GET` argument: `GET {{API_URL}}/api/v1/resolution_notes/` -# Update a resolution note +## Update a resolution note ```shell curl "{{API_URL}}/api/v1/resolution_notes/M4BTQUS3PRHYQ/" \ @@ -134,7 +136,7 @@ The above command returns JSON structured in the following way: `PUT {{API_URL}}/api/v1/resolution_notes//` -# Delete a resolution note +## Delete a resolution note ```shell curl "{{API_URL}}/api/v1/resolution_notes/M4BTQUS3PRHYQ/" \ diff --git a/docs/sources/oncall-api-reference/routes.md b/docs/sources/oncall-api-reference/routes.md index 81940477..1ecf5d4f 100644 --- a/docs/sources/oncall-api-reference/routes.md +++ b/docs/sources/oncall-api-reference/routes.md @@ -4,7 +4,9 @@ title: Routes HTTP API weight: 1100 --- -# Create a route +# Routes HTTP API + +## Create a route ```shell curl "{{API_URL}}/api/v1/routes/" \ @@ -57,7 +59,7 @@ Routes allow you to direct different alerts to different messenger channels and `POST {{API_URL}}/api/v1/routes/` -# Get a route +## Get a route ```shell curl "{{API_URL}}/api/v1/routes/RIYGUJXCPFHXY/" \ @@ -86,7 +88,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/routes//` -# List routes +## List routes ```shell curl "{{API_URL}}/api/v1/routes/" \ @@ -141,7 +143,7 @@ The following available filter parameters should be provided as `GET` arguments: `GET {{API_URL}}/api/v1/routes/` -# Update route +## Update route ```shell curl "{{API_URL}}/api/v1/routes/RIYGUJXCPFHXY/" \ @@ -177,7 +179,7 @@ The above command returns JSON structured in the following way: `PUT {{API_URL}}/api/v1/routes//` -# Delete a route +## Delete a route ```shell curl "{{API_URL}}/api/v1/routes/RIYGUJXCPFHXY/" \ diff --git a/docs/sources/oncall-api-reference/schedules.md b/docs/sources/oncall-api-reference/schedules.md index 228c8f07..5a2d80c8 100644 --- a/docs/sources/oncall-api-reference/schedules.md +++ b/docs/sources/oncall-api-reference/schedules.md @@ -1,10 +1,12 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/schedules/ -title: Schedule HTTP API +title: Schedules HTTP API weight: 1200 --- -# Create a schedule +# Schedules HTTP API + +## Create a schedule ```shell curl "{{API_URL}}/api/v1/schedules/" \ @@ -55,7 +57,7 @@ The above command returns JSON structured in the following way: `POST {{API_URL}}/api/v1/schedules/` -# Get a schedule +## Get a schedule ```shell curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \ @@ -86,7 +88,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/schedules//` -# List schedules +## List schedules ```shell curl "{{API_URL}}/api/v1/schedules/" \ @@ -145,7 +147,7 @@ The following available filter parameter should be provided as a `GET` argument: `GET {{API_URL}}/api/v1/schedules/` -# Update a schedule +## Update a schedule ```shell curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \ @@ -183,7 +185,7 @@ The above command returns JSON structured in the following way: `PUT {{API_URL}}/api/v1/schedules//` -# Delete a schedule +## Delete a schedule ```shell curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \ @@ -196,7 +198,7 @@ curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \ `DELETE {{API_URL}}/api/v1/schedules//` -# Export a schedule's final shifts +## Export a schedule's final shifts **HTTP request** @@ -305,7 +307,7 @@ The above command returns JSON structured in the following way: } ``` -## Caveats +### Caveats Some notes on the `start_date` and `end_date` query parameters: @@ -318,7 +320,7 @@ change the output you get from this endpoint. To get consistent information abou you must be sure to avoid updating rotations in-place but apply the changes as new rotations with the right starting dates. -## Example script to transform data to .csv for all of your schedules +### Example script to transform data to .csv for all of your schedules The following Python script will generate a `.csv` file, `oncall-report-2023-01-01-to-2023-01-31.csv`. This file will contain three columns, `user_pk`, `user_email`, and `hours_on_call`, which represents how many hours each user was diff --git a/docs/sources/oncall-api-reference/shift_swaps.md b/docs/sources/oncall-api-reference/shift_swaps.md index 7888218a..77ba958f 100644 --- a/docs/sources/oncall-api-reference/shift_swaps.md +++ b/docs/sources/oncall-api-reference/shift_swaps.md @@ -1,10 +1,12 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/shift_swaps/ -title: Shift swaps HTTP API +title: Shift swap requests HTTP API weight: 1200 --- -# Create a shift swap request +# Shift swap requests HTTP API + +## Create a shift swap request ```shell curl "{{API_URL}}/api/v1/shift_swaps/" \ @@ -77,7 +79,7 @@ The above command returns JSON structured in the following way: `POST {{API_URL}}/api/v1/shift_swaps/` -# Get a shift swap request +## Get a shift swap request ```shell curl "{{API_URL}}/api/v1/shift_swaps/SSRG1TDNBMJQ1NC/" \ @@ -135,7 +137,7 @@ The above command returns JSON structured in the following way: `GET {{API_URL}}/api/v1/shift_swaps//` -# List shift swap requests +## List shift swap requests ```shell curl "{{API_URL}}/api/v1/shift_swaps/" \ @@ -195,7 +197,7 @@ The following available filter parameters may be provided as a `GET` arguments: `GET {{API_URL}}/api/v1/shift_swaps/` -# Update a shift swap request +## Update a shift swap request ```shell curl "{{API_URL}}/api/v1/shift_swaps/SSRG1TDNBMJQ1NC/" \ @@ -259,7 +261,7 @@ The above command returns JSON structured in the following way: `PUT {{API_URL}}/api/v1/shift_swaps//` -# Delete a shift swap request +## Delete a shift swap request ```shell curl "{{API_URL}}/api/v1/shift_swaps/SSRG1TDNBMJQ1NC/" \ @@ -272,7 +274,7 @@ curl "{{API_URL}}/api/v1/shift_swaps/SSRG1TDNBMJQ1NC/" \ `DELETE {{API_URL}}/api/v1/shift_swaps//` -# Take a shift swap request +## Take a shift swap request ```shell curl "{{API_URL}}/api/v1/shift_swaps/SSRG1TDNBMJQ1NC/take" \ diff --git a/docs/sources/oncall-api-reference/slack_channels.md b/docs/sources/oncall-api-reference/slack_channels.md index e2c39b44..72036d4e 100644 --- a/docs/sources/oncall-api-reference/slack_channels.md +++ b/docs/sources/oncall-api-reference/slack_channels.md @@ -1,10 +1,12 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/slack_channels/ -title: Slack Channels HTTP API +title: Slack channels HTTP API weight: 1300 --- -# List Slack Channels +# Slack channels HTTP API + +## List Slack Channels ```shell curl "{{API_URL}}/api/v1/slack_channels/" \ diff --git a/docs/sources/oncall-api-reference/user_groups.md b/docs/sources/oncall-api-reference/user_groups.md index 49d58942..9a453251 100644 --- a/docs/sources/oncall-api-reference/user_groups.md +++ b/docs/sources/oncall-api-reference/user_groups.md @@ -1,12 +1,14 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/user_groups/ -title: OnCall User Groups HTTP API +title: OnCall user groups HTTP API weight: 1400 --- -# List user groups +# OnCall user groups HTTP API + +## List user groups ```shell curl "{{API_URL}}/api/v1/user_groups/" \ diff --git a/docs/sources/oncall-api-reference/users.md b/docs/sources/oncall-api-reference/users.md index 434a2afd..034b575e 100644 --- a/docs/sources/oncall-api-reference/users.md +++ b/docs/sources/oncall-api-reference/users.md @@ -1,10 +1,12 @@ --- canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/users/ -title: Grafana OnCall Users HTTP API +title: Grafana OnCall users HTTP API weight: 1500 --- -# Get a user +# Grafana OnCall users HTTP API + +## Get a user This endpoint retrieves the user object. @@ -50,7 +52,7 @@ Use `{{API_URL}}/api/v1/users/current` to retrieve the current user. | `timezone` | No | timezone of the user one of [time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). | | `teams` | No | List of team IDs the user belongs to | -# List Users +## List Users ```shell curl "{{API_URL}}/api/v1/users/" \ From 51594b0fb99bea58b990162e92d92f08877587fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:05:11 -0400 Subject: [PATCH 13/19] Bump urllib3 from 2.2.1 to 2.2.2 in /tools/migrators (#4550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2.
Release notes

Sourced from urllib3's releases.

2.2.2

🚀 urllib3 is fundraising for HTTP/2 support

urllib3 is raising ~$40,000 USD to release HTTP/2 support and ensure long-term sustainable maintenance of the project after a sharp decline in financial support for 2023. If your company or organization uses Python and would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and thousands of other projects please consider contributing financially to ensure HTTP/2 support is developed sustainably and maintained for the long-haul.

Thank you for your support.

Changes

  • Added the Proxy-Authorization header to the list of headers to strip from requests when redirecting to a different host. As before, different headers can be set via Retry.remove_headers_on_redirect.
  • Allowed passing negative integers as amt to read methods of http.client.HTTPResponse as an alternative to None. (#3122)
  • Fixed return types representing copying actions to use typing.Self. (#3363)

Full Changelog: https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2

Changelog

Sourced from urllib3's changelog.

2.2.2 (2024-06-17)

  • Added the Proxy-Authorization header to the list of headers to strip from requests when redirecting to a different host. As before, different headers can be set via Retry.remove_headers_on_redirect.
  • Allowed passing negative integers as amt to read methods of http.client.HTTPResponse as an alternative to None. ([#3122](https://github.com/urllib3/urllib3/issues/3122) <https://github.com/urllib3/urllib3/issues/3122>__)
  • Fixed return types representing copying actions to use typing.Self. ([#3363](https://github.com/urllib3/urllib3/issues/3363) <https://github.com/urllib3/urllib3/issues/3363>__)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=urllib3&package-manager=pip&previous-version=2.2.1&new-version=2.2.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/grafana/oncall/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tools/migrators/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/migrators/requirements.txt b/tools/migrators/requirements.txt index e73f413f..ce7823fc 100644 --- a/tools/migrators/requirements.txt +++ b/tools/migrators/requirements.txt @@ -34,7 +34,7 @@ requests==2.32.0 # pdpyras tomli==2.0.1 # via pytest -urllib3==2.2.1 +urllib3==2.2.2 # via # pdpyras # requests From 83a2bf3d42f46f8a3a597bef24e61221fa8f6a6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 10:45:26 -0400 Subject: [PATCH 14/19] Bump ws from 7.5.9 to 7.5.10 in /grafana-plugin (#4554) Bumps [ws](https://github.com/websockets/ws) from 7.5.9 to 7.5.10.
Release notes

Sourced from ws's releases.

7.5.10

Bug fixes

  • Backported e55e5106 to the 7.x release line (22c28763).
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ws&package-manager=npm_and_yarn&previous-version=7.5.9&new-version=7.5.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/grafana/oncall/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- grafana-plugin/yarn.lock | 44 +++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 499768d6..827b6c98 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -13459,7 +13459,16 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13577,7 +13586,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13598,6 +13607,13 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -14938,8 +14954,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14957,6 +14972,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -14990,14 +15014,14 @@ write-file-atomic@^4.0.2: signal-exit "^3.0.7" ws@^7.3.1: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== ws@^8.11.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" - integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^4.0.0: version "4.0.0" From 97ec7f1bfa0e67967e0e69c5eb7a424ba6135561 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Wed, 19 Jun 2024 15:18:19 -0600 Subject: [PATCH 15/19] Only generate random OnCall secrets value when it is not provided (#4563) # What this PR does New secret values were being generated for OnCall secrets `secretKey` and `mirageSecretKey` even when a fixed value was provided in the `values.yaml` file. This causes encryption of tokens to break in the DB through inconsistency when things are redeployed. This PR fixes it so that the value will only be generated if it is not set and the values in `values.yaml` are used. ## Which issue(s) this PR closes Closes [issue link here] ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- helm/oncall/templates/secrets.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/oncall/templates/secrets.yaml b/helm/oncall/templates/secrets.yaml index 40a02694..a4c32845 100644 --- a/helm/oncall/templates/secrets.yaml +++ b/helm/oncall/templates/secrets.yaml @@ -12,8 +12,8 @@ metadata: {{- end }} type: Opaque data: - {{ include "snippet.oncall.secret.secretKey" . }}: {{ randAlphaNum 40 | b64enc | quote }} - {{ include "snippet.oncall.secret.mirageSecretKey" . }}: {{ randAlphaNum 40 | b64enc | quote }} + {{ include "snippet.oncall.secret.secretKey" . }}: {{ (.Values.oncall.secrets.secretKey | default (randAlphaNum 40)) | b64enc | quote }} + {{ include "snippet.oncall.secret.mirageSecretKey" . }}: {{ (.Values.oncall.secrets.mirageSecretKey | default (randAlphaNum 40)) | b64enc | quote }} --- {{- end }} {{- if and (eq .Values.database.type "mysql") (not .Values.mariadb.enabled) (not .Values.externalMysql.existingSecret) }} From e2a719892bc8c70fd84b6334256ae3ef521e84bb Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 20 Jun 2024 11:59:34 +0200 Subject: [PATCH 16/19] Remove beta warning for mobile app (#4564) ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- docs/sources/set-up/open-source/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/sources/set-up/open-source/index.md b/docs/sources/set-up/open-source/index.md index 1fc1ef0d..375a7137 100644 --- a/docs/sources/set-up/open-source/index.md +++ b/docs/sources/set-up/open-source/index.md @@ -302,8 +302,6 @@ The limit can be changed using env variables: ## Mobile application set up -> **Note**: This application is currently in beta - Grafana OnCall OSS users can use the mobile app to receive push notifications from OnCall. Grafana OnCall OSS relies on Grafana Cloud as on relay for push notifications. You must first connect your Grafana OnCall OSS to Grafana Cloud for the mobile app to work. From 21ba1aa9e781afe7b059986ad5ce468ba7c8e053 Mon Sep 17 00:00:00 2001 From: Dominik Broj Date: Thu, 20 Jun 2024 13:54:07 +0200 Subject: [PATCH 17/19] initialize faro only on prod (#4566) # What this PR does initialize faro only on prod ## Which issue(s) this PR closes Closes https://github.com/grafana/oncall-private/issues/2756 ## Checklist - [ ] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- grafana-plugin/package.json | 6 +++--- grafana-plugin/src/utils/consts.ts | 13 ++++++++++++- grafana-plugin/src/utils/faro.ts | 3 ++- grafana-plugin/webpack.config.ts | 1 + 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index e39bb446..e888a52c 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -7,8 +7,8 @@ "lint:fix": "eslint --max-warnings=0 --fix --cache --ext .js,.jsx,.ts,.tsx ./src ./e2e-tests", "stylelint": "stylelint ./src/**/*.{css,scss,module.css,module.scss}", "stylelint:fix": "stylelint --fix ./src/**/*.{css,scss,module.css,module.scss}", - "build": "webpack -c ./webpack.config.ts --env production", - "build:dev": "webpack -c ./webpack.config.ts --env development", + "build": "NODE_ENV=production webpack -c ./webpack.config.ts --env production", + "build:dev": "NODE_ENV=development webpack -c ./webpack.config.ts --env development", "labels:link": "yarn --cwd ../../gops-labels/frontend link && yarn link \"@grafana/labels\" && yarn --cwd ../../gops-labels/frontend watch", "labels:unlink": "yarn --cwd ../../gops-labels/frontend unlink", "test-utc": "TZ=UTC jest --verbose --testNamePattern '^((?!@london-tz).)*$'", @@ -23,7 +23,7 @@ "test:e2e:gen": "yarn playwright codegen http://localhost:3000", "e2e-show-report": "yarn playwright show-report", "generate-types": "cd ./src/network/oncall-api/types-generator && yarn generate", - "watch": "webpack -w -c ./webpack.config.ts --env development", + "watch": "NODE_ENV=development webpack -w -c ./webpack.config.ts --env development", "sign": "npx --yes @grafana/sign-plugin@latest", "start": "yarn watch", "plop": "plop", diff --git a/grafana-plugin/src/utils/consts.ts b/grafana-plugin/src/utils/consts.ts index ee691a70..7e26bae2 100644 --- a/grafana-plugin/src/utils/consts.ts +++ b/grafana-plugin/src/utils/consts.ts @@ -35,12 +35,23 @@ export const ONCALL_PROD = 'https://oncall-prod-us-central-0.grafana.net/oncall' export const ONCALL_OPS = 'https://oncall-ops-us-east-0.grafana.net/oncall'; export const ONCALL_DEV = 'https://oncall-dev-us-central-0.grafana.net/oncall'; +export const getProcessEnvVarSafely = (name: string) => { + try { + return process.env[name]; + } catch (error) { + console.error(error); + return undefined; + } +}; + +export const getIsDevelopmentEnv = () => getProcessEnvVarSafely['NODE_ENV'] === 'development'; + // Single source of truth on the frontend for OnCall API URL export const getOnCallApiUrl = (meta?: OnCallAppPluginMeta) => { if (meta?.jsonData?.onCallApiUrl) { return meta?.jsonData?.onCallApiUrl; } else if (typeof window === 'undefined') { - return process.env.ONCALL_API_URL; + return getProcessEnvVarSafely('ONCALL_API_URL'); } return undefined; }; diff --git a/grafana-plugin/src/utils/faro.ts b/grafana-plugin/src/utils/faro.ts index 782511aa..6b4c9df3 100644 --- a/grafana-plugin/src/utils/faro.ts +++ b/grafana-plugin/src/utils/faro.ts @@ -9,6 +9,7 @@ import { ONCALL_DEV, ONCALL_OPS, ONCALL_PROD, + getIsDevelopmentEnv, } from './consts'; import { safeJSONStringify } from './string'; @@ -31,7 +32,7 @@ class BaseFaroHelper { faro: Faro; initializeFaro(onCallApiUrl: string) { - if (this.faro) { + if (this.faro || getIsDevelopmentEnv()) { return undefined; } diff --git a/grafana-plugin/webpack.config.ts b/grafana-plugin/webpack.config.ts index 49c66ee9..cccf4a11 100644 --- a/grafana-plugin/webpack.config.ts +++ b/grafana-plugin/webpack.config.ts @@ -64,6 +64,7 @@ const config = async (env): Promise => { ...(env.development ? [new LiveReloadPlugin({ appendScriptTag: true, useSourceHash: true })] : []), new EnvironmentPlugin({ ONCALL_API_URL: null, + NODE_ENV: 'development', }), new DefinePlugin({ 'process.env': JSON.stringify(dotenv.config().parsed), From 7455966b896b366fe6e6de5baf6f3b55f8a08bbe Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Thu, 20 Jun 2024 10:09:24 -0600 Subject: [PATCH 18/19] Add a simple phone number ban mechanism (#4557) # What this PR does Add a simple list for maintaining phone numbers to restrict from SMS, voice and verify. Works by removing the number as verified and block future verification attempts with that number rather than check every operation since all operations already check if a number is verified. ## Which issue(s) this PR closes ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- engine/apps/phone_notifications/exceptions.py | 4 ++ .../migrations/0002_bannedphonenumber.py | 22 ++++++ .../models/banned_phone_number.py | 61 +++++++++++++++++ .../apps/phone_notifications/phone_backend.py | 3 + .../tests/test_banned_phone_number.py | 41 ++++++++++++ .../test_phone_backend_phone_verification.py | 67 ++++++++++++++++++- 6 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 engine/apps/phone_notifications/migrations/0002_bannedphonenumber.py create mode 100644 engine/apps/phone_notifications/models/banned_phone_number.py create mode 100644 engine/apps/phone_notifications/tests/test_banned_phone_number.py diff --git a/engine/apps/phone_notifications/exceptions.py b/engine/apps/phone_notifications/exceptions.py index a605ac81..c7c38f12 100644 --- a/engine/apps/phone_notifications/exceptions.py +++ b/engine/apps/phone_notifications/exceptions.py @@ -48,3 +48,7 @@ class CallsLimitExceeded(Exception): class SMSLimitExceeded(Exception): pass + + +class PhoneNumberBanned(Exception): + pass diff --git a/engine/apps/phone_notifications/migrations/0002_bannedphonenumber.py b/engine/apps/phone_notifications/migrations/0002_bannedphonenumber.py new file mode 100644 index 00000000..025dbaf2 --- /dev/null +++ b/engine/apps/phone_notifications/migrations/0002_bannedphonenumber.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.11 on 2024-06-19 21:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('phone_notifications', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='BannedPhoneNumber', + fields=[ + ('phone_number', models.CharField(max_length=20, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now=True)), + ('reason', models.TextField(default=None, null=True)), + ('users', models.JSONField(default=None, null=True)), + ], + ), + ] diff --git a/engine/apps/phone_notifications/models/banned_phone_number.py b/engine/apps/phone_notifications/models/banned_phone_number.py new file mode 100644 index 00000000..a6d6511c --- /dev/null +++ b/engine/apps/phone_notifications/models/banned_phone_number.py @@ -0,0 +1,61 @@ +import json +import logging +from dataclasses import asdict, dataclass +from typing import List + +from django.db import models + +from apps.phone_notifications.exceptions import PhoneNumberBanned + +logger = logging.getLogger(__name__) + + +@dataclass +class BannedPhoneUserEntry: + user_id: int + user_name: str + org_id: int + stack_slug: str + org_slug: str + + +class BannedPhoneNumber(models.Model): + phone_number = models.CharField(primary_key=True, max_length=20) + created_at = models.DateTimeField(auto_now=True) + reason = models.TextField(null=True, default=None) + users = models.JSONField(null=True, default=None) + + def get_user_entries(self) -> List[BannedPhoneUserEntry]: + return [BannedPhoneUserEntry(**data) for data in json.loads(self.users)] + + +def ban_phone_number(phone_number: str, reason: str): + from apps.user_management.models import User + + banned_phone_number = BannedPhoneNumber(phone_number=phone_number) + users = User.objects.filter(_verified_phone_number=phone_number) + # Record instances of phone number use + user_entries = [ + asdict( + BannedPhoneUserEntry( + user_id=user.id, + user_name=user.username, + org_id=user.organization.org_id, + stack_slug=user.organization.stack_slug, + org_slug=user.organization.org_slug, + ) + ) + for user in users + ] + users.update(_verified_phone_number=None) + banned_phone_number.users = json.dumps(user_entries) + banned_phone_number.reason = reason + banned_phone_number.save() + + logger.info(f"ban_phone_number={phone_number}, in use by users={len(user_entries)}, reason={reason}") + + +def check_banned_phone_number(phone_number: str): + banned_entry = BannedPhoneNumber.objects.filter(phone_number=phone_number).first() + if banned_entry: + raise PhoneNumberBanned diff --git a/engine/apps/phone_notifications/phone_backend.py b/engine/apps/phone_notifications/phone_backend.py index 26b64399..a8cf50d6 100644 --- a/engine/apps/phone_notifications/phone_backend.py +++ b/engine/apps/phone_notifications/phone_backend.py @@ -21,6 +21,7 @@ from .exceptions import ( SMSLimitExceeded, ) from .models import PhoneCallRecord, ProviderPhoneCall, ProviderSMS, SMSRecord +from .models.banned_phone_number import check_banned_phone_number from .phone_provider import PhoneProvider, get_phone_provider logger = logging.getLogger(__name__) @@ -316,6 +317,7 @@ class PhoneBackend: if self._validate_user_number(user): logger.info(f"PhoneBackend.send_verification_sms: number already verified for user {user.id}") raise NumberAlreadyVerified + check_banned_phone_number(user.unverified_phone_number) self.phone_provider.send_verification_sms(user.unverified_phone_number) def make_verification_call(self, user): @@ -327,6 +329,7 @@ class PhoneBackend: if self._validate_user_number(user): logger.info(f"PhoneBackend.make_verification_call: number already verified user_id={user.id}") raise NumberAlreadyVerified + check_banned_phone_number(user.unverified_phone_number) self.phone_provider.make_verification_call(user.unverified_phone_number) def verify_phone_number(self, user, code) -> bool: diff --git a/engine/apps/phone_notifications/tests/test_banned_phone_number.py b/engine/apps/phone_notifications/tests/test_banned_phone_number.py new file mode 100644 index 00000000..8363a790 --- /dev/null +++ b/engine/apps/phone_notifications/tests/test_banned_phone_number.py @@ -0,0 +1,41 @@ +import pytest + +from apps.phone_notifications.models.banned_phone_number import BannedPhoneNumber, ban_phone_number + + +@pytest.mark.django_db +def test_ban_phone_number(make_organization, make_user_for_organization): + organization = make_organization() + banned_phone_number = "+1234567890" + unbanned_phone_number = "+0987654321" + banned_user_1 = make_user_for_organization( + organization=organization, + _verified_phone_number=banned_phone_number, + unverified_phone_number=banned_phone_number, + ) + banned_user_2 = make_user_for_organization( + organization=organization, + _verified_phone_number=banned_phone_number, + unverified_phone_number=banned_phone_number, + ) + unbanned_user = make_user_for_organization( + organization=organization, + _verified_phone_number=unbanned_phone_number, + unverified_phone_number=unbanned_phone_number, + ) + reason = "usage too high" + ban_phone_number(banned_phone_number, reason) + banned_user_1.refresh_from_db() + assert banned_user_1._verified_phone_number is None + assert banned_user_1.unverified_phone_number == banned_phone_number + banned_user_2.refresh_from_db() + assert banned_user_2._verified_phone_number is None + assert banned_user_2.unverified_phone_number == banned_phone_number + unbanned_user.refresh_from_db() + assert unbanned_user._verified_phone_number == unbanned_phone_number + assert unbanned_user.unverified_phone_number == unbanned_phone_number + ban_phone_number_entry = BannedPhoneNumber.objects.get(pk=banned_phone_number) + assert ban_phone_number_entry is not None + assert ban_phone_number_entry.reason == reason + user_entries = ban_phone_number_entry.get_user_entries() + assert len(user_entries) == 2 diff --git a/engine/apps/phone_notifications/tests/test_phone_backend_phone_verification.py b/engine/apps/phone_notifications/tests/test_phone_backend_phone_verification.py index cfb8996c..a12380cb 100644 --- a/engine/apps/phone_notifications/tests/test_phone_backend_phone_verification.py +++ b/engine/apps/phone_notifications/tests/test_phone_backend_phone_verification.py @@ -2,7 +2,8 @@ from unittest import mock import pytest -from apps.phone_notifications.exceptions import NumberAlreadyVerified +from apps.phone_notifications.exceptions import NumberAlreadyVerified, PhoneNumberBanned +from apps.phone_notifications.models.banned_phone_number import ban_phone_number from apps.phone_notifications.phone_backend import PhoneBackend from apps.phone_notifications.tests.mock_phone_provider import MockPhoneProvider @@ -67,3 +68,67 @@ def test_make_verification_call_raises_when_number_verified( user.save_verified_phone_number("+1234567890") with pytest.raises(NumberAlreadyVerified): phone_backend.make_verification_call(user) + + +@pytest.mark.django_db +@mock.patch("apps.phone_notifications.phone_backend.PhoneBackend._validate_user_number", return_value=False) +@mock.patch("apps.phone_notifications.tests.mock_phone_provider.MockPhoneProvider.send_verification_sms") +def test_send_verification_sms_banned_number( + mock_send_verification_sms, mock_validate_user_number, make_organization_and_user +): + _, user = make_organization_and_user() + phone_backend = PhoneBackend() + + number_to_verify = "+1234567890" + user.unverified_phone_number = "+1234567890" + ban_phone_number(number_to_verify, "usage too high") + with pytest.raises(PhoneNumberBanned): + phone_backend.send_verification_sms(user) + + +@pytest.mark.django_db +@mock.patch("apps.phone_notifications.phone_backend.PhoneBackend._validate_user_number", return_value=False) +@mock.patch("apps.phone_notifications.tests.mock_phone_provider.MockPhoneProvider.send_verification_sms") +def test_send_verification_sms_unaffected_by_ban( + mock_send_verification_sms, mock_validate_user_number, make_organization_and_user +): + _, user = make_organization_and_user() + phone_backend = PhoneBackend() + + number_to_verify = "+1234567890" + user.unverified_phone_number = "+1234567890" + ban_phone_number("+0987654321", "usage too high") + phone_backend.send_verification_sms(user) + mock_send_verification_sms.assert_called_once_with(number_to_verify) + + +@pytest.mark.django_db +@mock.patch("apps.phone_notifications.phone_backend.PhoneBackend._validate_user_number", return_value=False) +@mock.patch("apps.phone_notifications.tests.mock_phone_provider.MockPhoneProvider.make_verification_call") +def test_make_verification_call_banned_number( + mock_make_verification_call, mock_validate_user_number, make_organization_and_user +): + _, user = make_organization_and_user() + phone_backend = PhoneBackend() + + number_to_verify = "+1234567890" + user.unverified_phone_number = "+1234567890" + ban_phone_number(number_to_verify, "usage too high") + with pytest.raises(PhoneNumberBanned): + phone_backend.make_verification_call(user) + + +@pytest.mark.django_db +@mock.patch("apps.phone_notifications.phone_backend.PhoneBackend._validate_user_number", return_value=False) +@mock.patch("apps.phone_notifications.tests.mock_phone_provider.MockPhoneProvider.make_verification_call") +def test_make_verification_call_unaffected_by_ban( + mock_make_verification_call, mock_validate_user_number, make_organization_and_user +): + _, user = make_organization_and_user() + phone_backend = PhoneBackend() + + number_to_verify = "+1234567890" + user.unverified_phone_number = "+1234567890" + ban_phone_number("+0987654321", "usage too high") + phone_backend.make_verification_call(user) + mock_make_verification_call.assert_called_once_with(number_to_verify) From d0ec5960461abdb417e8565e36cd348a8d232c65 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Thu, 20 Jun 2024 11:12:26 -0600 Subject: [PATCH 19/19] Update README.md Remove link for forum we are no longer using --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8a44f55f..e83f0f14 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ [![License](https://img.shields.io/github/license/grafana/oncall)](https://github.com/grafana/oncall/blob/dev/LICENSE) [![Docker Pulls](https://img.shields.io/docker/pulls/grafana/oncall)](https://hub.docker.com/r/grafana/oncall/tags) [![Slack](https://img.shields.io/badge/join%20slack-%23grafana-%2Doncall-brightgreen.svg)](https://slack.grafana.com/) -[![Discussion](https://img.shields.io/badge/discuss-oncall%20forum-orange.svg)](https://github.com/grafana/oncall/discussions) [![Build Status](https://github.com/grafana/oncall/actions/workflows/on-commits-to-dev.yml/badge.svg)](https://github.com/grafana/oncall/actions/workflows/on-commits-to-dev.yml) Developer-friendly incident response with brilliant Slack integration.