diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fc43310..56583ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +## v1.2.39 (2023-06-06) + +### Changed + +- Do not hide not secret settings in the web plugin UI by @alexintech ([#1964](https://github.com/grafana/oncall/pull/1964)) + ## v1.2.36 (2023-06-02) +### Added + +- Add public API endpoint to export a schedule's final shifts by @joeyorlando ([2047](https://github.com/grafana/oncall/pull/2047)) + ### Fixed - Fix demo alert for inbound email integration by @vadimkerr ([#2081](https://github.com/grafana/oncall/pull/2081)) diff --git a/docs/sources/_index.md b/docs/sources/_index.md index bb0c2a60..8fa2c245 100644 --- a/docs/sources/_index.md +++ b/docs/sources/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/ canonical: https://grafana.com/docs/oncall/latest/ keywords: - Grafana Cloud diff --git a/docs/sources/escalation-chains-and-routes/_index.md b/docs/sources/escalation-chains-and-routes/_index.md index 5045fb28..61bff7c9 100644 --- a/docs/sources/escalation-chains-and-routes/_index.md +++ b/docs/sources/escalation-chains-and-routes/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/escalation-chains-and-routes/ canonical: https://grafana.com/docs/oncall/latest/escalation-chains-and-routes/ title: Escalation Chains and Routes weight: 600 diff --git a/docs/sources/get-started/_index.md b/docs/sources/get-started/_index.md index ab962482..ac4ac21b 100644 --- a/docs/sources/get-started/_index.md +++ b/docs/sources/get-started/_index.md @@ -1,6 +1,5 @@ --- aliases: - - /docs/oncall/latest/get-started/ - /getting-started/ canonical: https://grafana.com/docs/oncall/latest/get-started/ keywords: diff --git a/docs/sources/integrations/_index.md b/docs/sources/integrations/_index.md index da9dec7d..822df7e6 100644 --- a/docs/sources/integrations/_index.md +++ b/docs/sources/integrations/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/integration-with-alert-sources/ canonical: https://grafana.com/docs/oncall/latest/integration-with-alert-sources/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/alertmanager/index.md b/docs/sources/integrations/alertmanager/index.md index df0e6f5d..d4912972 100644 --- a/docs/sources/integrations/alertmanager/index.md +++ b/docs/sources/integrations/alertmanager/index.md @@ -1,7 +1,6 @@ --- aliases: - add-alertmanager/ - - /docs/oncall/latest/integrations/available-integrations/configure-alertmanager/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-alertmanager/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/grafana-alerting/index.md b/docs/sources/integrations/grafana-alerting/index.md index 776fed61..a5fe7679 100644 --- a/docs/sources/integrations/grafana-alerting/index.md +++ b/docs/sources/integrations/grafana-alerting/index.md @@ -1,7 +1,6 @@ --- aliases: - add-grafana-alerting/ - - /docs/oncall/latest/integrations/available-integrations/configure-grafana-alerting/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-grafana-alerting/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/inbound-email/index.md b/docs/sources/integrations/inbound-email/index.md index c96fce3c..6c4c6b31 100644 --- a/docs/sources/integrations/inbound-email/index.md +++ b/docs/sources/integrations/inbound-email/index.md @@ -1,7 +1,6 @@ --- aliases: - inbound-email/ - - /docs/oncall/latest/integrations/available-integrations/configure-inbound-email/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-inbound-email/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/webhook/index.md b/docs/sources/integrations/webhook/index.md index 3052e5cb..66c03496 100644 --- a/docs/sources/integrations/webhook/index.md +++ b/docs/sources/integrations/webhook/index.md @@ -1,7 +1,6 @@ --- aliases: - ../add-webhook-integration/ - - /docs/oncall/latest/integrations/available-integrations/configure-webhook/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-webhook/ keywords: - Grafana Cloud diff --git a/docs/sources/integrations/zabbix/index.md b/docs/sources/integrations/zabbix/index.md index a5359743..38714a68 100644 --- a/docs/sources/integrations/zabbix/index.md +++ b/docs/sources/integrations/zabbix/index.md @@ -1,7 +1,6 @@ --- aliases: - add-zabbix/ - - /docs/oncall/latest/integrations/available-integrations/configure-zabbix/ canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-zabbix/ keywords: - Grafana Cloud diff --git a/docs/sources/jinja2-templating/_index.md b/docs/sources/jinja2-templating/_index.md index 5664821f..7fcd876c 100644 --- a/docs/sources/jinja2-templating/_index.md +++ b/docs/sources/jinja2-templating/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/jinja2-templating/ canonical: https://grafana.com/docs/oncall/latest/jinja2-templating/ title: Jinja2 templating weight: 1000 diff --git a/docs/sources/mobile-app/_index.md b/docs/sources/mobile-app/_index.md index be482075..d6d3d7b2 100644 --- a/docs/sources/mobile-app/_index.md +++ b/docs/sources/mobile-app/_index.md @@ -1,7 +1,5 @@ --- title: Mobile App -aliases: - - /docs/oncall/latest/mobile-app/ canonical: https://grafana.com/docs/oncall/latest/mobile-app/ keywords: - Mobile App diff --git a/docs/sources/notify/ms-teams/index.md b/docs/sources/notify/ms-teams/index.md index 602ef4d4..94821845 100644 --- a/docs/sources/notify/ms-teams/index.md +++ b/docs/sources/notify/ms-teams/index.md @@ -1,7 +1,6 @@ --- aliases: - ../../chat-options/configure-teams/ - - /docs/oncall/latest/integrations/chatops-integrations/configure-teams/ canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-teams/ keywords: - Grafana Cloud diff --git a/docs/sources/notify/slack/index.md b/docs/sources/notify/slack/index.md index e3f10238..9ad7594a 100644 --- a/docs/sources/notify/slack/index.md +++ b/docs/sources/notify/slack/index.md @@ -1,7 +1,6 @@ --- aliases: - ../../chat-options/configure-slack/ - - /docs/oncall/latest/integrations/chatops-integrations/configure-slack/ canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-slack/ keywords: - Grafana Cloud diff --git a/docs/sources/notify/telegram/index.md b/docs/sources/notify/telegram/index.md index 17080b2c..5b8dbcd2 100644 --- a/docs/sources/notify/telegram/index.md +++ b/docs/sources/notify/telegram/index.md @@ -1,7 +1,6 @@ --- aliases: - ../../chat-options/configure-telegram/ - - /docs/oncall/latest/integrations/chatops-integrations/configure-telegram/ canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-telegram/ keywords: - Grafana Cloud diff --git a/docs/sources/on-call-schedules/ical-schedules/index.md b/docs/sources/on-call-schedules/ical-schedules/index.md index 79b9dd75..d802f888 100644 --- a/docs/sources/on-call-schedules/ical-schedules/index.md +++ b/docs/sources/on-call-schedules/ical-schedules/index.md @@ -1,14 +1,13 @@ --- title: iCal on-call schedules -aliases: - - /docs/oncall/latest/on-call-schedules/ical-schedules/ canonical: https://grafana.com/docs/oncall/latest/on-call-schedules/ical-schedules/ description: "Learn how to manage on-call schedules with iCal import" keywords: - Grafana - oncall - on-call - - calendar + - calendar +title: Import on-call schedules weight: 300 --- diff --git a/docs/sources/on-call-schedules/web-schedule/_index.md b/docs/sources/on-call-schedules/web-schedule/_index.md index 582e1579..e29d7cad 100644 --- a/docs/sources/on-call-schedules/web-schedule/_index.md +++ b/docs/sources/on-call-schedules/web-schedule/_index.md @@ -1,7 +1,5 @@ --- title: Web-based on-call schedules -aliases: - - /docs/oncall/latest/on-call-schedules/web-schedule/ canonical: https://grafana.com/docs/oncall/latest/on-call-schedules/web-schedule/ description: "Learn more about Grafana OnCalls built in schedule tool" keywords: @@ -9,6 +7,7 @@ keywords: - oncall - schedule - calendar +title: Web-based schedules weight: 100 --- diff --git a/docs/sources/oncall-api-reference/_index.md b/docs/sources/oncall-api-reference/_index.md index 12ae513a..e289f07b 100644 --- a/docs/sources/oncall-api-reference/_index.md +++ b/docs/sources/oncall-api-reference/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/ title: Grafana OnCall HTTP API reference weight: 1500 diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index 5f5addf8..f0d68157 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/alertgroups/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/alertgroups/ title: Alert groups HTTP API weight: 400 diff --git a/docs/sources/oncall-api-reference/alerts.md b/docs/sources/oncall-api-reference/alerts.md index ab4b6a3a..cfc19114 100644 --- a/docs/sources/oncall-api-reference/alerts.md +++ b/docs/sources/oncall-api-reference/alerts.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/alerts/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/alerts/ title: Alerts HTTP API weight: 100 diff --git a/docs/sources/oncall-api-reference/escalation_chains.md b/docs/sources/oncall-api-reference/escalation_chains.md index f88b8d71..301deb2a 100644 --- a/docs/sources/oncall-api-reference/escalation_chains.md +++ b/docs/sources/oncall-api-reference/escalation_chains.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/escalation_chains/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_chains/ title: Escalation Chains HTTP API weight: 200 diff --git a/docs/sources/oncall-api-reference/escalation_policies.md b/docs/sources/oncall-api-reference/escalation_policies.md index 0cca0501..1c538759 100644 --- a/docs/sources/oncall-api-reference/escalation_policies.md +++ b/docs/sources/oncall-api-reference/escalation_policies.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/escalation_policies/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_policies/ title: Escalation Policies HTTP API weight: 300 diff --git a/docs/sources/oncall-api-reference/integrations.md b/docs/sources/oncall-api-reference/integrations.md index a4af73b8..00f4702c 100644 --- a/docs/sources/oncall-api-reference/integrations.md +++ b/docs/sources/oncall-api-reference/integrations.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/integrations/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/integrations/ title: Integrations HTTP API weight: 500 diff --git a/docs/sources/oncall-api-reference/on_call_shifts.md b/docs/sources/oncall-api-reference/on_call_shifts.md index ff70f9c2..74917bf0 100644 --- a/docs/sources/oncall-api-reference/on_call_shifts.md +++ b/docs/sources/oncall-api-reference/on_call_shifts.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/on_call_shifts/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/on_call_shifts/ title: OnCall shifts HTTP API weight: 600 diff --git a/docs/sources/oncall-api-reference/outgoing_webhooks.md b/docs/sources/oncall-api-reference/outgoing_webhooks.md index deb26150..20bd59f0 100644 --- a/docs/sources/oncall-api-reference/outgoing_webhooks.md +++ b/docs/sources/oncall-api-reference/outgoing_webhooks.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/outgoing_webhooks/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/outgoing_webhooks/ title: Outgoing webhooks HTTP API weight: 700 diff --git a/docs/sources/oncall-api-reference/personal_notification_rules.md b/docs/sources/oncall-api-reference/personal_notification_rules.md index cff10373..01fe45d0 100644 --- a/docs/sources/oncall-api-reference/personal_notification_rules.md +++ b/docs/sources/oncall-api-reference/personal_notification_rules.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/personal_notification_rules/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/personal_notification_rules/ title: Personal Notification Rules HTTP API weight: 800 diff --git a/docs/sources/oncall-api-reference/postmortem_messages.md b/docs/sources/oncall-api-reference/postmortem_messages.md index f3f5aff8..237b7bff 100644 --- a/docs/sources/oncall-api-reference/postmortem_messages.md +++ b/docs/sources/oncall-api-reference/postmortem_messages.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/postmortem_messages/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/postmortem_messages/ draft: true title: Postmortem Messages HTTP API diff --git a/docs/sources/oncall-api-reference/postmortems.md b/docs/sources/oncall-api-reference/postmortems.md index 91797f74..41ae758c 100644 --- a/docs/sources/oncall-api-reference/postmortems.md +++ b/docs/sources/oncall-api-reference/postmortems.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/postmortems/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/postmortems/ draft: true title: Postmortem HTTP API diff --git a/docs/sources/oncall-api-reference/routes.md b/docs/sources/oncall-api-reference/routes.md index 49dfc25a..4d8987a2 100644 --- a/docs/sources/oncall-api-reference/routes.md +++ b/docs/sources/oncall-api-reference/routes.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/routes/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/routes/ title: Routes HTTP API weight: 1100 diff --git a/docs/sources/oncall-api-reference/schedules.md b/docs/sources/oncall-api-reference/schedules.md index 4be429b7..9f9410a9 100644 --- a/docs/sources/oncall-api-reference/schedules.md +++ b/docs/sources/oncall-api-reference/schedules.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/schedules/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/schedules/ title: Schedule HTTP API weight: 1200 @@ -197,3 +195,171 @@ curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \ **HTTP request** `DELETE {{API_URL}}/api/v1/schedules//` + +# Export a schedule's final shifts + +**HTTP request** + +```shell +curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/final_shifts?start_date=2023-01-01&end_date=2023-02-01" \ + --request GET \ + --header "Authorization: meowmeowmeow" +``` + +The above command returns JSON structured in the following way: + +```json +{ + "count": 12, + "next": null, + "previous": null, + "results": [ + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-02T09:00:00Z", + "shift_end": "2023-01-02T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-04T09:00:00Z", + "shift_end": "2023-01-04T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-06T09:00:00Z", + "shift_end": "2023-01-06T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-09T09:00:00Z", + "shift_end": "2023-01-09T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-11T09:00:00Z", + "shift_end": "2023-01-11T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-13T09:00:00Z", + "shift_end": "2023-01-13T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-16T09:00:00Z", + "shift_end": "2023-01-16T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-18T09:00:00Z", + "shift_end": "2023-01-18T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-20T09:00:00Z", + "shift_end": "2023-01-20T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-23T09:00:00Z", + "shift_end": "2023-01-23T17:00:00Z" + }, + { + "user_pk": "UC2CHRT5SD34X", + "user_email": "alice@example.com", + "user_username": "alice", + "shift_start": "2023-01-25T09:00:00Z", + "shift_end": "2023-01-25T17:00:00Z" + }, + { + "user_pk": "U7S8H84ARFTGN", + "user_email": "bob@example.com", + "user_username": "bob", + "shift_start": "2023-01-27T09:00:00Z", + "shift_end": "2023-01-27T17:00:00Z" + } + ] +} +``` + +## Caveats + +Some notes on the `start_date` and `end_date` query parameters: + +- they are both required and should represent ISO 8601 formatted dates +- `end_date` must be greater than or equal to `start_date` +- `end_date` cannot be more than 365 days in the future from `start_date` + +Lastly, this endpoint is currently only active for web schedules. It will return HTTP 400 for schedules +defined via Terraform or iCal. + +## 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 +on call during the period starting January 1, 2023 to January 31, 2023 (inclusive). + +```python +import csv +import requests +from datetime import datetime + +# CUSTOMIZE THE FOLLOWING VARIABLES +START_DATE = "2023-01-01" +END_DATE = "2023-01-31" +OUTPUT_FILE_NAME = f"oncall-report-{START_DATE}-to-{END_DATE}.csv" +MY_ONCALL_API_BASE_URL = "https://oncall-prod-us-central-0.grafana.net/oncall/api/v1/schedules" +MY_ONCALL_API_KEY = "meowmeowwoofwoof" + +headers = {"Authorization": MY_ONCALL_API_KEY} +schedule_ids = [schedule["id"] for schedule in requests.get(MY_ONCALL_API_BASE_URL, headers=headers).json()["results"]] +user_on_call_hours = {} + +for schedule_id in schedule_ids: + response = requests.get( + f"{MY_ONCALL_API_BASE_URL}/{schedule_id}/final_shifts?start_date={START_DATE}&end_date={END_DATE}", + headers=headers) + + for final_shift in response.json()["results"]: + user_pk = final_shift["user_pk"] + end = datetime.fromisoformat(final_shift["shift_end"]) + start = datetime.fromisoformat(final_shift["shift_start"]) + shift_time_in_seconds = (end - start).total_seconds() + shift_time_in_hours = shift_time_in_seconds / (60 * 60) + + if user_pk in user_on_call_hours: + user_on_call_hours[user_pk]["hours_on_call"] += shift_time_in_hours + else: + user_on_call_hours[user_pk] = { + "email": final_shift["user_email"], + "hours_on_call": shift_time_in_hours, + } + +with open(OUTPUT_FILE_NAME, "w") as fp: + csv_writer = csv.DictWriter(fp, ["user_pk", "user_email", "hours_on_call"]) + csv_writer.writeheader() + + for user_pk, user_info in user_on_call_hours.items(): + csv_writer.writerow({ + "user_pk": user_pk, "user_email": user_info["email"], "hours_on_call": user_info["hours_on_call"]}) +``` diff --git a/docs/sources/oncall-api-reference/slack_channels.md b/docs/sources/oncall-api-reference/slack_channels.md index f5516688..82e78152 100644 --- a/docs/sources/oncall-api-reference/slack_channels.md +++ b/docs/sources/oncall-api-reference/slack_channels.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/slack_channels/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/slack_channels/ title: Slack Channels HTTP API weight: 1300 diff --git a/docs/sources/oncall-api-reference/user_groups.md b/docs/sources/oncall-api-reference/user_groups.md index 2c5966a0..b7f6e328 100644 --- a/docs/sources/oncall-api-reference/user_groups.md +++ b/docs/sources/oncall-api-reference/user_groups.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/user_groups/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/user_groups/ title: OnCall User Groups HTTP API weight: 1400 diff --git a/docs/sources/oncall-api-reference/users.md b/docs/sources/oncall-api-reference/users.md index 4c520eeb..9d205fb0 100644 --- a/docs/sources/oncall-api-reference/users.md +++ b/docs/sources/oncall-api-reference/users.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/oncall-api-reference/users/ canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/users/ title: Grafana OnCall Users HTTP API weight: 1500 diff --git a/docs/sources/open-source/_index.md b/docs/sources/open-source/_index.md index 1674e9be..c0c92316 100644 --- a/docs/sources/open-source/_index.md +++ b/docs/sources/open-source/_index.md @@ -1,6 +1,4 @@ --- -aliases: - - /docs/oncall/latest/open-source/ keywords: - Open Source title: Open Source diff --git a/docs/sources/outgoing-webhooks/_index.md b/docs/sources/outgoing-webhooks/_index.md index 7b80a8da..a871b624 100644 --- a/docs/sources/outgoing-webhooks/_index.md +++ b/docs/sources/outgoing-webhooks/_index.md @@ -1,7 +1,6 @@ --- aliases: - ../outgoing-webhooks/ - - /docs/oncall/latest/outgoing-webhooks/ canonical: https://grafana.com/docs/oncall/latest/outgoing-webhooks/ keywords: - Grafana Cloud diff --git a/docs/sources/user-and-team-management/_index.md b/docs/sources/user-and-team-management/_index.md index 07a78dc1..447f0b5c 100644 --- a/docs/sources/user-and-team-management/_index.md +++ b/docs/sources/user-and-team-management/_index.md @@ -1,7 +1,5 @@ --- title: User and team management -aliases: - - /docs/oncall/latest/user-and-team-management/ keywords: - oncall - RBAC diff --git a/engine/Dockerfile b/engine/Dockerfile index ad1bbd6c..0b8c98ad 100644 --- a/engine/Dockerfile +++ b/engine/Dockerfile @@ -7,7 +7,9 @@ RUN apt-get update && apt-get install -y \ netcat \ curl \ bash \ - git + git \ + libpcre3 \ + libpcre3-dev WORKDIR /etc/app COPY ./requirements.txt ./ diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index c6e66a09..f22baddb 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -195,6 +195,7 @@ class OnCallShiftUpdateSerializer(OnCallShiftSerializer): validated_data = self._correct_validated_data(instance.type, validated_data) change_only_title = True create_or_update_last_shift = False + force_update = validated_data.pop("force_update", True) for field in validated_data: if field != "title" and validated_data[field] != getattr(instance, field): @@ -209,7 +210,7 @@ class OnCallShiftUpdateSerializer(OnCallShiftSerializer): elif instance.event_is_finished: raise serializers.ValidationError(["This event cannot be updated"]) - if create_or_update_last_shift: + if not force_update and create_or_update_last_shift: result = instance.create_or_update_last_shift(validated_data) else: result = super().update(instance, validated_data) diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index d92f4c4f..94c1eba2 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -402,6 +402,56 @@ def test_update_started_on_call_shift( assert on_call_shift.until == on_call_shift.updated_shift.rotation_start +@pytest.mark.django_db +def test_update_started_on_call_shift_force_update( + on_call_shift_internal_api_setup, + make_on_call_shift, + make_user_auth_headers, +): + token, user1, _, _, schedule = on_call_shift_internal_api_setup + + client = APIClient() + start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) + + title = "Test Shift Rotation" + on_call_shift = make_on_call_shift( + schedule.organization, + shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, + schedule=schedule, + title=title, + start=start_date, + duration=timezone.timedelta(hours=3), + rotation_start=start_date, + rolling_users=[{user1.pk: user1.public_primary_key}], + ) + data_to_update = { + "title": title, + "priority_level": 2, + "shift_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "shift_end": (start_date + timezone.timedelta(hours=1)).strftime("%Y-%m-%dT%H:%M:%SZ"), + "rotation_start": start_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "until": None, + "frequency": None, + "interval": None, + "by_day": None, + "rolling_users": [[user1.public_primary_key]], + } + + assert on_call_shift.priority_level != data_to_update["priority_level"] + + url = reverse("api-internal:oncall_shifts-detail", kwargs={"pk": on_call_shift.public_primary_key}) + "?force=true" + + response = client.put(url, data=data_to_update, format="json", **make_user_auth_headers(user1, token)) + + assert response.status_code == status.HTTP_200_OK + # check no shift was created + assert response.data["id"] == on_call_shift.public_primary_key + on_call_shift.refresh_from_db() + assert on_call_shift.priority_level == data_to_update["priority_level"] + assert on_call_shift.updated_shift is None + assert on_call_shift.until is None + + @pytest.mark.django_db def test_update_old_on_call_shift_with_future_version( on_call_shift_internal_api_setup, @@ -1363,7 +1413,9 @@ def test_on_call_shift_preview( "is_gap": False, "priority_level": 2, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "source": "web", } ] @@ -1653,7 +1705,9 @@ def test_on_call_shift_preview_update( "is_gap": False, "priority_level": 1, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "source": "web", } assert rotation_events[-1] == expected_shift_preview @@ -1764,7 +1818,9 @@ def test_on_call_shift_preview_update_not_started_reuse_pk( "is_gap": False, "priority_level": 1, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "source": "web", }, ] diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 7a00c7ef..5a574894 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -812,7 +812,7 @@ def test_events_calendar( "all_day": False, "start": on_call_shift.start, "end": on_call_shift.start + on_call_shift.duration, - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", @@ -878,7 +878,7 @@ def test_filter_events_calendar( "all_day": False, "start": mon_start, "end": mon_start + on_call_shift.duration, - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", @@ -894,7 +894,7 @@ def test_filter_events_calendar( "all_day": False, "start": fri_start, "end": fri_start + on_call_shift.duration, - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", @@ -977,7 +977,7 @@ def test_filter_events_range_calendar( "all_day": False, "start": fri_start, "end": fri_start + on_call_shift.duration, - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "missing_users": [], "priority_level": on_call_shift.priority_level, "source": "api", @@ -1059,7 +1059,13 @@ def test_filter_events_overrides( "all_day": False, "start": override_start, "end": override_start + override.duration, - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + { + "display_name": other_user.username, + "pk": other_user.public_primary_key, + "email": other_user.email, + } + ], "missing_users": [], "priority_level": None, "source": "api", diff --git a/engine/apps/api/views/on_call_shifts.py b/engine/apps/api/views/on_call_shifts.py index b24482c1..c760b53f 100644 --- a/engine/apps/api/views/on_call_shifts.py +++ b/engine/apps/api/views/on_call_shifts.py @@ -71,7 +71,8 @@ class OnCallShiftView(TeamFilteringMixin, PublicPrimaryKeyMixin, UpdateSerialize def perform_update(self, serializer): prev_state = serializer.instance.insight_logs_serialized - serializer.save() + force_update = self.request.query_params.get("force", "") == "true" + serializer.save(force_update=force_update) new_state = serializer.instance.insight_logs_serialized write_resource_insight_log( instance=serializer.instance, diff --git a/engine/apps/public_api/serializers/schedules_base.py b/engine/apps/public_api/serializers/schedules_base.py index d69c620c..42742cb4 100644 --- a/engine/apps/public_api/serializers/schedules_base.py +++ b/engine/apps/public_api/serializers/schedules_base.py @@ -71,3 +71,17 @@ class ScheduleBaseSerializer(serializers.ModelSerializer): } return result + + +class FinalShiftQueryParamsSerializer(serializers.Serializer): + start_date = serializers.DateField(required=True) + end_date = serializers.DateField(required=True) + + def validate(self, attrs): + if attrs["start_date"] > attrs["end_date"]: + raise serializers.ValidationError("start_date must be less than or equal to end_date") + if attrs["end_date"] - attrs["start_date"] > timezone.timedelta(days=365): + raise serializers.ValidationError( + "The difference between start_date and end_date must be less than one year (365 days)" + ) + return attrs diff --git a/engine/apps/public_api/tests/test_schedules.py b/engine/apps/public_api/tests/test_schedules.py index b4a40279..1e5a2ba5 100644 --- a/engine/apps/public_api/tests/test_schedules.py +++ b/engine/apps/public_api/tests/test_schedules.py @@ -1,3 +1,4 @@ +import collections from unittest.mock import patch import pytest @@ -781,3 +782,146 @@ def test_create_ical_schedule_without_ical_url(make_organization_and_user_with_t } response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_oncall_shifts_request_validation( + make_organization_and_user_with_token, + make_schedule, +): + organization, _, token = make_organization_and_user_with_token() + ical_schedule = make_schedule(organization, schedule_class=OnCallScheduleICal) + terraform_schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) + web_schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + + schedule_type_validation_msg = "OnCall shifts exports are currently only available for web calendars" + valid_date_msg = "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." + + client = APIClient() + + def _make_request(schedule, query_params=""): + url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key}) + return client.get(f"{url}{query_params}", format="json", HTTP_AUTHORIZATION=token) + + # only web schedules are allowed for now + response = _make_request(ical_schedule) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == schedule_type_validation_msg + + response = _make_request(terraform_schedule) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == schedule_type_validation_msg + + # query param validation + response = _make_request(web_schedule, "?start_date=2021-01-01") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["end_date"][0] == "This field is required." + + response = _make_request(web_schedule, "?start_date=asdfasdf") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["start_date"][0] == valid_date_msg + + response = _make_request(web_schedule, "?end_date=2021-01-01") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["start_date"][0] == "This field is required." + + response = _make_request(web_schedule, "?start_date=2021-01-01&end_date=asdfasdf") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["end_date"][0] == valid_date_msg + + response = _make_request(web_schedule, "?end_date=2021-01-01&start_date=2022-01-01") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == { + "non_field_errors": [ + "start_date must be less than or equal to end_date", + ] + } + + response = _make_request(web_schedule, "?end_date=2021-01-01&start_date=2019-12-31") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == { + "non_field_errors": [ + "The difference between start_date and end_date must be less than one year (365 days)", + ] + } + + +@pytest.mark.django_db +def test_oncall_shifts_export( + make_organization_and_user_with_token, + make_user, + make_schedule, + make_on_call_shift, +): + organization, _, token = make_organization_and_user_with_token() + + user1_email = "alice909450945045@example.com" + user2_email = "bob123123123123123@example.com" + user1_username = "alice" + user2_username = "bob" + + user1 = make_user(organization=organization, email=user1_email, username=user1_username) + user2 = make_user(organization=organization, email=user2_email, username=user2_username) + + user1_public_primary_key = user1.public_primary_key + user2_public_primary_key = user2.public_primary_key + schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) + + start_date = timezone.datetime(2023, 1, 1, 9, 0, 0) + make_on_call_shift( + organization=organization, + schedule=schedule, + shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, + frequency=CustomOnCallShift.FREQUENCY_DAILY, + priority_level=1, + interval=1, + by_day=["MO", "WE", "FR"], + start=start_date, + until=start_date + timezone.timedelta(days=28), + rolling_users=[{user1.pk: user1_public_primary_key}, {user2.pk: user2_public_primary_key}], + rotation_start=start_date, + duration=timezone.timedelta(hours=8), + ) + + client = APIClient() + + url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key}) + response = client.get(f"{url}?start_date=2023-01-01&end_date=2023-02-01", format="json", HTTP_AUTHORIZATION=token) + response_json = response.json() + shifts = response_json["results"] + + total_time_on_call = collections.defaultdict(int) + pk_to_user_mapping = { + user1_public_primary_key: { + "email": user1_email, + "username": user1_username, + }, + user2_public_primary_key: { + "email": user2_email, + "username": user2_username, + }, + } + + for row in shifts: + user_pk = row["user_pk"] + + # make sure we're exporting email and username as well + assert pk_to_user_mapping[user_pk]["email"] == row["user_email"] + assert pk_to_user_mapping[user_pk]["username"] == row["user_username"] + + end = timezone.datetime.fromisoformat(row["shift_end"]) + start = timezone.datetime.fromisoformat(row["shift_start"]) + shift_time_in_seconds = (end - start).total_seconds() + total_time_on_call[row["user_pk"]] += shift_time_in_seconds / (60 * 60) + + assert response.status_code == status.HTTP_200_OK + + # 3 shifts per week x 4 weeks x 8 hours per shift = 96 / 2 users = 48h per user for this period + expected_time_on_call = 48 + assert total_time_on_call[user1_public_primary_key] == expected_time_on_call + assert total_time_on_call[user2_public_primary_key] == expected_time_on_call + + # pagination parameters are mocked out for now + assert response_json["next"] is None + assert response_json["previous"] is None + assert response_json["count"] == len(shifts) diff --git a/engine/apps/public_api/views/schedules.py b/engine/apps/public_api/views/schedules.py index 39d5d26d..ab3aec8e 100644 --- a/engine/apps/public_api/views/schedules.py +++ b/engine/apps/public_api/views/schedules.py @@ -1,3 +1,5 @@ +import logging + from django_filters import rest_framework as filters from rest_framework import status from rest_framework.decorators import action @@ -9,9 +11,11 @@ from rest_framework.viewsets import ModelViewSet from apps.auth_token.auth import ApiTokenAuthentication, ScheduleExportAuthentication from apps.public_api.custom_renderers import CalendarRenderer from apps.public_api.serializers import PolymorphicScheduleSerializer, PolymorphicScheduleUpdateSerializer +from apps.public_api.serializers.schedules_base import FinalShiftQueryParamsSerializer from apps.public_api.throttlers.user_throttle import UserThrottle from apps.schedules.ical_utils import ical_export_from_schedule from apps.schedules.models import OnCallSchedule, OnCallScheduleWeb +from apps.schedules.models.on_call_schedule import ScheduleEvents, ScheduleFinalShifts from apps.slack.tasks import update_slack_user_group_for_schedules from common.api_helpers.exceptions import BadRequest from common.api_helpers.filters import ByTeamFilter @@ -19,6 +23,8 @@ from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMix from common.api_helpers.paginators import FiftyPageSizePaginator from common.insight_log import EntityEvent, write_resource_insight_log +logger = logging.getLogger(__name__) + class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): authentication_classes = (ApiTokenAuthentication,) @@ -120,3 +126,51 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo # Not using existing get_object method because it requires access to the organization user attribute export = ical_export_from_schedule(self.request.auth.schedule) return Response(export, status=status.HTTP_200_OK) + + @action(methods=["get"], detail=True) + def final_shifts(self, request, pk): + schedule = self.get_object() + + if not isinstance(schedule, OnCallScheduleWeb): + return Response( + "OnCall shifts exports are currently only available for web calendars", + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = FinalShiftQueryParamsSerializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + + start_date = serializer.validated_data["start_date"] + end_date = serializer.validated_data["end_date"] + days_between_start_and_end = (end_date - start_date).days + + final_schedule_events: ScheduleEvents = schedule.final_events("UTC", start_date, days_between_start_and_end) + + logger.info( + f"Exporting oncall shifts for schedule {pk} between dates {start_date} and {end_date}. {len(final_schedule_events)} shift events were found." + ) + + data: ScheduleFinalShifts = [ + { + "user_pk": user["pk"], + "user_email": user["email"], + "user_username": user["display_name"], + "shift_start": event["start"], + "shift_end": event["end"], + } + for event in final_schedule_events + for user in event["users"] + ] + + # right now we'll "mock out" the pagination related parameters (next and previous) + # rather than use a Pagination class from drf (as currently it operates on querysets). We've decided on this + # to make this response schema consistent with the rest of the public API + make it easy to add pagination + # here in the future (should we decide to migrate "final_shifts" to an actual model) + return Response( + { + "count": len(data), + "next": None, + "previous": None, + "results": data, + } + ) diff --git a/engine/apps/schedules/models/on_call_schedule.py b/engine/apps/schedules/models/on_call_schedule.py index 816784c8..0f1f99de 100644 --- a/engine/apps/schedules/models/on_call_schedule.py +++ b/engine/apps/schedules/models/on_call_schedule.py @@ -76,6 +76,7 @@ class QualityReport(TypedDict): class ScheduleEventUser(TypedDict): display_name: str pk: str + email: str class ScheduleEventShift(TypedDict): @@ -97,8 +98,17 @@ class ScheduleEvent(TypedDict): shift: ScheduleEventShift +class ScheduleFinalShift(TypedDict): + user_pk: str + user_email: str + user_username: str + shift_start: str + shift_end: str + + ScheduleEvents = List[ScheduleEvent] ScheduleEventIntervals = List[List[datetime.datetime]] +ScheduleFinalShifts = List[ScheduleFinalShift] def generate_public_primary_key_for_oncall_schedule_channel(): @@ -323,6 +333,7 @@ class OnCallSchedule(PolymorphicModel): "users": [ { "display_name": user.username, + "email": user.email, "pk": user.public_primary_key, } for user in shift["users"] diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index 3e7e7121..a61c7432 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -93,7 +93,7 @@ def test_filter_events(make_organization, make_user_for_organization, make_sched "is_gap": False, "priority_level": on_call_shift.priority_level, "missing_users": [], - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "shift": {"pk": on_call_shift.public_primary_key}, "source": "api", } @@ -114,7 +114,7 @@ def test_filter_events(make_organization, make_user_for_organization, make_sched "is_gap": False, "priority_level": None, "missing_users": [], - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "shift": {"pk": override.public_primary_key}, "source": "api", } @@ -179,7 +179,7 @@ def test_filter_events_include_gaps(make_organization, make_user_for_organizatio "is_gap": False, "priority_level": on_call_shift.priority_level, "missing_users": [], - "users": [{"display_name": user.username, "pk": user.public_primary_key}], + "users": [{"display_name": user.username, "pk": user.public_primary_key, "email": user.email}], "shift": {"pk": on_call_shift.public_primary_key}, "source": "api", }, @@ -688,7 +688,9 @@ def test_preview_shift(make_organization, make_user_for_organization, make_sched "is_gap": False, "priority_level": new_shift.priority_level, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "shift": {"pk": new_shift.public_primary_key}, "source": "api", } @@ -846,7 +848,9 @@ def test_preview_override_shift(make_organization, make_user_for_organization, m "is_gap": False, "priority_level": None, "missing_users": [], - "users": [{"display_name": other_user.username, "pk": other_user.public_primary_key}], + "users": [ + {"display_name": other_user.username, "pk": other_user.public_primary_key, "email": other_user.email} + ], "shift": {"pk": new_shift.public_primary_key}, "source": "api", } diff --git a/engine/engine/middlewares.py b/engine/engine/middlewares.py index 83ca2bf6..5af7fc31 100644 --- a/engine/engine/middlewares.py +++ b/engine/engine/middlewares.py @@ -3,9 +3,8 @@ import logging from django.apps import apps from django.conf import settings -from django.core.exceptions import PermissionDenied, RequestDataTooBig +from django.core.exceptions import PermissionDenied from django.db import OperationalError -from django.http import HttpResponse from django.utils.deprecation import MiddlewareMixin logger = logging.getLogger(__name__) @@ -57,19 +56,6 @@ class RequestTimeLoggingMiddleware(MiddlewareMixin): return response -class RequestBodyReadingMiddleware(MiddlewareMixin): - def process_request(self, request): - # Reading request body, as required by uwsgi - # https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html - # "If an HTTP request has a body (like a POST request generated by a form), - # you have to read (consume) it in your application. - # If you do not do this, the communication socket with your webserver may be clobbered." - try: - request.body - except RequestDataTooBig: - return HttpResponse(status=400) - - class BanAlertConsumptionBasedOnSettingsMiddleware(MiddlewareMixin): """ Banning requests for /integrations/v1 diff --git a/engine/settings/base.py b/engine/settings/base.py index ce6ec554..25056d8d 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -245,7 +245,6 @@ MIDDLEWARE = [ "log_request_id.middleware.RequestIDMiddleware", "engine.middlewares.RequestTimeLoggingMiddleware", "engine.middlewares.BanAlertConsumptionBasedOnSettingsMiddleware", - "engine.middlewares.RequestBodyReadingMiddleware", "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware", diff --git a/engine/uwsgi.ini b/engine/uwsgi.ini index dbc489f1..2425c79a 100644 --- a/engine/uwsgi.ini +++ b/engine/uwsgi.ini @@ -17,6 +17,9 @@ http-timeout=620 post-buffering=1 enable-threads=true +; drop requests with CONTENT_LENGTH bigger than 15MB +route-if=ishigher:${CONTENT_LENGTH};15000000 break:413 Request Entity Too Large + logger=stdio log-format=source=engine:uwsgi status=%(status) method=%(method) path=%(uri) latency=%(secs) google_trace_id=%(var.HTTP_X_CLOUD_TRACE_CONTEXT) protocol=%(proto) resp_size=%(size) req_body_size=%(cl) log-encoder=format ${strftime:%%Y-%%m-%%d %%H:%%M:%%S} ${msgnl} diff --git a/grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx b/grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx index b93662e6..e6f6da03 100644 --- a/grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx +++ b/grafana-plugin/src/pages/settings/tabs/LiveSettings/LiveSettingsPage.tsx @@ -166,7 +166,7 @@ class LiveSettings extends React.Component onTextChange={this.getEditValueChangeHandler(item)} editable={isUserActionAllowed(UserActions.OtherSettingsWrite)} clearBeforeEdit={item.is_secret} - hidden={hideValues} + hidden={hideValues && item.is_secret} > {normalizeValue(item.value)} @@ -208,7 +208,7 @@ class LiveSettings extends React.Component return (
- {hideValues ? PLACEHOLDER : normalizeValue(item.default_value)} + {hideValues && item.is_secret ? PLACEHOLDER : normalizeValue(item.default_value)}
); };