From a1517debfa8c118c96bb5667f5ccf7a2f828b09f Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:43:38 +0100 Subject: [PATCH 001/172] Docs: Specify permissions for creating public dashboards (#113086) Co-authored-by: jtvdez --- .../share-dashboards-panels/shared-dashboards/index.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md b/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md index a96efda3edf..b0dc5e723b2 100644 --- a/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md +++ b/docs/sources/visualizations/dashboards/share-dashboards-panels/shared-dashboards/index.md @@ -37,6 +37,11 @@ refs: destination: /docs/grafana//introduction/grafana-enterprise/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//introduction/grafana-enterprise/ + fixed-role-definitions: + - pattern: /docs/grafana/ + destination: /docs/grafana//administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/#fixed-role-definitions + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-control/rbac-fixed-basic-role-definitions/#fixed-role-definitions --- # Externally shared dashboards @@ -49,6 +54,8 @@ Externally shared dashboards allow you to share your Grafana dashboard with anyo If you change a dashboard, ensure that you save the changes before sharing. +In order to create public dashboards, you need to be an Admin, have the `dashboards.public:write` permission, or the `fixed:dashboards.public:write` [RBAC role](ref:fixed-role-definitions). + {{< admonition type="warning" >}} Sharing your dashboard externally could result in a large number of queries to the data sources used by your dashboard. This can be mitigated by using the Enterprise [caching](ref:caching) and/or rate limiting features. From 802e208440753367f49398bc47c83c314ec997e9 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:41:23 +0000 Subject: [PATCH 002/172] I18n: Download translations from Crowdin (#114987) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 15 ++++++++++----- public/locales/de-DE/grafana.json | 13 ++++++++++--- public/locales/es-ES/grafana.json | 13 ++++++++++--- public/locales/fr-FR/grafana.json | 13 ++++++++++--- public/locales/hu-HU/grafana.json | 13 ++++++++++--- public/locales/id-ID/grafana.json | 12 ++++++++++-- public/locales/it-IT/grafana.json | 13 ++++++++++--- public/locales/ja-JP/grafana.json | 12 ++++++++++-- public/locales/ko-KR/grafana.json | 12 ++++++++++-- public/locales/nl-NL/grafana.json | 13 ++++++++++--- public/locales/pl-PL/grafana.json | 15 ++++++++++----- public/locales/pt-BR/grafana.json | 13 ++++++++++--- public/locales/pt-PT/grafana.json | 13 ++++++++++--- public/locales/ru-RU/grafana.json | 15 ++++++++++----- public/locales/sv-SE/grafana.json | 13 ++++++++++--- public/locales/tr-TR/grafana.json | 13 ++++++++++--- public/locales/zh-Hans/grafana.json | 12 ++++++++++-- public/locales/zh-Hant/grafana.json | 12 ++++++++++-- 18 files changed, 180 insertions(+), 55 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index e90a588053c..6ce52307b99 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -2350,11 +2350,8 @@ "label-tenant-sources": "Zdroje tenanta" }, "rule-details-matching-instances": { - "showing-count": "Zobrazuje se {{visibleItems}} z {{totalItems}} instancí", - "button-show-all_one": "Zobrazit všechny instance výstrah: {{totalItems}}", - "button-show-all_few": "Zobrazit všechny instance výstrah: {{totalItems}}", - "button-show-all_many": "Zobrazit všechny instance výstrah: {{totalItems}}", - "button-show-all_other": "Zobrazit všechny instance výstrah: {{totalItems}}" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8324,6 +8321,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9324,6 +9327,8 @@ "actions-confirmation-label": "Potvrzovací zpráva", "actions-confirmation-message": "Napište popisnou výzvu k potvrzení nebo zrušení akce.", "footer-add-annotation": "Přidat vysvětlivku", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Klikněte pro {{actionTitle}}", "footer-click-to-navigate": "Klikněte pro otevření {{linkTitle}}", "footer-filter-for-value": "Filtrovat podle hodnoty „{{value}}“", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index e0a149baab7..5619e7c8ab5 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Tenant-Quellen" }, "rule-details-matching-instances": { - "showing-count": "Angezeigt werden {{visibleItems}} von {{totalItems}} Instanzen", - "button-show-all_one": "Alle {{totalItems}} Warninstanzen anzeigen", - "button-show-all_other": "Alle {{totalItems}} Warninstanzen anzeigen" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Bestätigungsnachricht", "actions-confirmation-message": "Geben Sie eine beschreibende Eingabeaufforderung an, um die Aktion zu bestätigen oder abzubrechen.", "footer-add-annotation": "Anmerkung hinzufügen", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Klicken, um zu {{actionTitle}}", "footer-click-to-navigate": "Zum Öffnen von {{linkTitle}} klicken", "footer-filter-for-value": "Filtern nach „{{value}}“", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index ff65f840041..dc07b47b5c6 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Fuentes de inquilinos" }, "rule-details-matching-instances": { - "showing-count": "Mostrando {{visibleItems}} de {{totalItems}} instancias", - "button-show-all_one": "Mostrar {{totalItems}} instancia de alerta", - "button-show-all_other": "Mostrar las {{totalItems}} instancias de alerta" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Mensaje de confirmación", "actions-confirmation-message": "Proporciona un mensaje descriptivo para confirmar o cancelar la acción.", "footer-add-annotation": "Añadir anotación", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Haz clic para {{actionTitle}}", "footer-click-to-navigate": "Haz clic para abrir {{linkTitle}}", "footer-filter-for-value": "Filtrar por «{{value}}»", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 01bbb43fff9..4adc55064e4 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Sources du locataire" }, "rule-details-matching-instances": { - "showing-count": "Affichage de {{visibleItems}} sur {{totalItems}} instances", - "button-show-all_one": "Afficher toutes les {{totalItems}} instances d’alerte", - "button-show-all_other": "Afficher toutes les {{totalItems}} instances d’alerte" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Message de confirmation", "actions-confirmation-message": "Fournissez une invite descriptive pour confirmer ou annuler l'action.", "footer-add-annotation": "Ajouter une annotation", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Cliquez pour {{actionTitle}}", "footer-click-to-navigate": "Cliquez pour ouvrir {{linkTitle}}", "footer-filter-for-value": "Filtrer par « {{value}} »", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 146b2a78d04..409c3249166 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Bérlői források" }, "rule-details-matching-instances": { - "showing-count": "{{totalItems}}/{{visibleItems}} példány megjelenítése", - "button-show-all_one": "Mind a(z) {{totalItems}} riasztáspéldány megjelenítése", - "button-show-all_other": "Mind a(z) {{totalItems}} riasztáspéldány megjelenítése" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Megerősítési üzenet", "actions-confirmation-message": "Adjon meg egy leíró kérdést a művelet megerősítéséhez vagy visszavonásához.", "footer-add-annotation": "Jegyzet hozzáadása", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Kattintson ehhez: {{actionTitle}}", "footer-click-to-navigate": "Kattintson a következő megnyitásához: {{linkTitle}}", "footer-filter-for-value": "Szűrés „{{value}}” szerint", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 7b8e043e947..75f6c32dcba 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -2326,8 +2326,8 @@ "label-tenant-sources": "Sumber tenant" }, "rule-details-matching-instances": { - "showing-count": "Menampilkan {{visibleItems}} dari {{totalItems}} instans", - "button-show-all_other": "Tampilkan semua {{totalItems}} instans peringatan" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8216,6 +8216,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9216,6 +9222,8 @@ "actions-confirmation-label": "Pesan konfirmasi", "actions-confirmation-message": "Berikan petunjuk deskriptif untuk mengonfirmasi atau membatalkan tindakan.", "footer-add-annotation": "Tambahkan anotasi", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Klik untuk {{actionTitle}} ", "footer-click-to-navigate": "Klik untuk membuka {{linkTitle}}", "footer-filter-for-value": "Filter untuk '{{value}}'", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 68364a78d6f..aa13b629973 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Origini tenant" }, "rule-details-matching-instances": { - "showing-count": "Mostra {{visibleItems}} di {{totalItems}} istanze", - "button-show-all_one": "Mostra tutte le {{totalItems}} istanze di avviso", - "button-show-all_other": "Mostra tutte le {{totalItems}} istanze di avviso" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Messaggio di conferma", "actions-confirmation-message": "Fornisci un prompt descrittivo per confermare o annullare l'azione.", "footer-add-annotation": "Aggiungi annotazione", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Fai clic per {{actionTitle}}", "footer-click-to-navigate": "Fai clic per aprire {{linkTitle}}", "footer-filter-for-value": "Filtra per \"{{value}}\"", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index c023581e3cf..f24c3eaa678 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -2326,8 +2326,8 @@ "label-tenant-sources": "テナントソース" }, "rule-details-matching-instances": { - "showing-count": "{{totalItems}}件中{{visibleItems}}件のインスタンスを表示", - "button-show-all_other": "すべてのアラートインスタンス({{totalItems}}件)を表示" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8216,6 +8216,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9216,6 +9222,8 @@ "actions-confirmation-label": "確認メッセージ", "actions-confirmation-message": "操作を確認またはキャンセルするための説明的なプロンプトを提供します。", "footer-add-annotation": "注釈を追加", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "クリックして{{actionTitle}}", "footer-click-to-navigate": "クリックして{{linkTitle}}を開く", "footer-filter-for-value": "{{value}}値でフィルタリング", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index e9cb03b8350..51978f07fb9 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -2326,8 +2326,8 @@ "label-tenant-sources": "테넌트 소스" }, "rule-details-matching-instances": { - "showing-count": "{{totalItems}}개 인스턴스 중 {{visibleItems}}개 표시 중", - "button-show-all_other": "모든 {{totalItems}} 경고 인스턴스 표시" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8216,6 +8216,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9216,6 +9222,8 @@ "actions-confirmation-label": "확인 메시지", "actions-confirmation-message": "작업을 확인하거나 취소하기 위한 설명 프롬프트를 제공합니다.", "footer-add-annotation": "주석 추가", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "클릭하여 {{actionTitle}} 작업 수행", "footer-click-to-navigate": "클릭하여 {{linkTitle}} 열기", "footer-filter-for-value": "'{{value}}'으(로) 필터링", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 7872baf058a..96765f7630e 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Tenantbronnen" }, "rule-details-matching-instances": { - "showing-count": "{{visibleItems}} van {{totalItems}} instanties worden weergegeven", - "button-show-all_one": "Alle {{totalItems}} waarschuwingsinstanties weergeven", - "button-show-all_other": "Alle {{totalItems}} waarschuwingsinstanties weergeven" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Bevestigingsbericht", "actions-confirmation-message": "Geef een beschrijvende prompt om de actie te bevestigen of te annuleren.", "footer-add-annotation": "Annotatie toevoegen", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Klik om te {{actionTitle}}", "footer-click-to-navigate": "Klik om {{linkTitle}} te openen", "footer-filter-for-value": "Filteren op '{{value}}'", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index acd3b23e77f..96fe66f6ae0 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -2350,11 +2350,8 @@ "label-tenant-sources": "Źródła najemców" }, "rule-details-matching-instances": { - "showing-count": "Wyświetlanie {{visibleItems}} z {{totalItems}} instancji", - "button-show-all_one": "Pokaż wszystkie {{totalItems}} instancji alertów", - "button-show-all_few": "Pokaż wszystkie {{totalItems}} instancji alertów", - "button-show-all_many": "Pokaż wszystkie {{totalItems}} instancji alertów", - "button-show-all_other": "Pokaż wszystkie {{totalItems}} instancji alertów" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8324,6 +8321,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9324,6 +9327,8 @@ "actions-confirmation-label": "Wiadomość z potwierdzeniem", "actions-confirmation-message": "Wyślij prośbę o potwierdzenie lub anulowanie działania.", "footer-add-annotation": "Dodaj komentarz", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Kliknij, aby podjąć działanie: {{actionTitle}}", "footer-click-to-navigate": "Kliknij, aby otworzyć {{linkTitle}}", "footer-filter-for-value": "Filtruj wg wartości „{{value}}”", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index e091960845a..dc5bd71fadc 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Fontes de locatários" }, "rule-details-matching-instances": { - "showing-count": "Mostrando {{visibleItems}} de {{totalItems}} instâncias", - "button-show-all_one": "Exibir {{totalItems}} instância de alerta", - "button-show-all_other": "Exibir {{totalItems}} instância de alerta" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Mensagem de confirmação", "actions-confirmation-message": "Forneça um prompt descritivo para confirmar ou cancelar a ação.", "footer-add-annotation": "Adicionar anotação", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Clique para {{actionTitle}}", "footer-click-to-navigate": "Clique para abrir {{linkTitle}}", "footer-filter-for-value": "Filtrar por \"{{value}}\"", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 3a69ffa3194..cfe6b435e73 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Origens de inquilinos" }, "rule-details-matching-instances": { - "showing-count": "A mostrar {{visibleItems}} de {{totalItems}} instâncias", - "button-show-all_one": "Mostrar todas as instâncias de alerta {{totalItems}}", - "button-show-all_other": "Mostrar todas as instâncias de alerta {{totalItems}}" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Mensagem de confirmação", "actions-confirmation-message": "Forneça uma mensagem descritiva para confirmar ou cancelar a ação.", "footer-add-annotation": "Adicionar anotação", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Clique para {{actionTitle}}", "footer-click-to-navigate": "Clique para abrir {{linkTitle}}", "footer-filter-for-value": "Filtrar por \"{{value}}\"", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index a85c8b00dd5..56fc00e836c 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -2350,11 +2350,8 @@ "label-tenant-sources": "Источники клиента" }, "rule-details-matching-instances": { - "showing-count": "Показаны {{visibleItems}} из {{totalItems}} экземпляров", - "button-show-all_one": "Показать все {{totalItems}} экземпляр оповещения", - "button-show-all_few": "Показать все {{totalItems}} экземпляра оповещения", - "button-show-all_many": "Показать все {{totalItems}} экземпляров оповещения", - "button-show-all_other": "Показать все {{totalItems}} экземпляра оповещения" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8324,6 +8321,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9324,6 +9327,8 @@ "actions-confirmation-label": "Сообщение о подтверждении", "actions-confirmation-message": "Предоставляет описательную подсказку для подтверждения или отмены действия.", "footer-add-annotation": "Добавить аннотацию", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Нажмите, чтобы {{actionTitle}}", "footer-click-to-navigate": "Нажмите, чтобы открыть {{linkTitle}}", "footer-filter-for-value": "Фильтр по «{{value}}»", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 733bfdd230b..ed1313039a7 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Klientkällor" }, "rule-details-matching-instances": { - "showing-count": "Visar {{visibleItems}} av {{totalItems}} instanser", - "button-show-all_one": "Visa alla {{totalItems}} larminstanser", - "button-show-all_other": "Visa alla {{totalItems}} larminstanser" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Bekräftelsemeddelande", "actions-confirmation-message": "Tillhandahåll en beskrivande prompt för att bekräfta eller avbryta åtgärden.", "footer-add-annotation": "Lägg till kommentar", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "Klicka för att {{actionTitle}}", "footer-click-to-navigate": "Klicka för att öppna {{linkTitle}}", "footer-filter-for-value": "Filtrera för ”{{value}}”", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 4b26d75fcb5..8f574384a75 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -2334,9 +2334,8 @@ "label-tenant-sources": "Kiracı kaynakları" }, "rule-details-matching-instances": { - "showing-count": "{{visibleItems}}/{{totalItems}} örnek gösteriliyor", - "button-show-all_one": "{{totalItems}} uyarı örneğinin tamamını göster", - "button-show-all_other": "{{totalItems}} uyarı örneğinin tamamını göster" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8252,6 +8251,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9252,6 +9257,8 @@ "actions-confirmation-label": "Onay mesajı", "actions-confirmation-message": "İşlemi onaylamak veya iptal etmek için açıklayıcı bir istem sağlayın.", "footer-add-annotation": "Ek açıklama ekle", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "{{actionTitle}} için tıklayın", "footer-click-to-navigate": "{{linkTitle}} ögesini açmak için tıklayın", "footer-filter-for-value": "'{{value}}' değerine göre filtrele", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 45cbea59c45..302ea2645f3 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -2326,8 +2326,8 @@ "label-tenant-sources": "租户源" }, "rule-details-matching-instances": { - "showing-count": "显示 {{visibleItems}} 个实例,共 {{totalItems}} 个", - "button-show-all_other": "显示所有 {{totalItems}} 个警报实例" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8216,6 +8216,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9216,6 +9222,8 @@ "actions-confirmation-label": "确认消息", "actions-confirmation-message": "提供描述性提示以确认或取消操作。", "footer-add-annotation": "添加注释", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "点击以 {{actionTitle}}", "footer-click-to-navigate": "点击打开 {{linkTitle}}", "footer-filter-for-value": "筛选“{{value}}”", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 5548f9f3d56..a0beece480a 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -2326,8 +2326,8 @@ "label-tenant-sources": "租用戶來源" }, "rule-details-matching-instances": { - "showing-count": "顯示 {{visibleItems}} 個執行個體,共 {{totalItems}} 個", - "button-show-all_other": "顯示所有 {{totalItems}} 個警報執行個體" + "button-show-all": "", + "showing-count": "" }, "rule-editor": { "get-content": { @@ -8216,6 +8216,12 @@ "nextNYears_other": "" } }, + "field": { + "fieldColor": { + "accessibleGroup": "", + "otherGroup": "" + } + }, "valueFormats": { "categories": { "acceleration": { @@ -9216,6 +9222,8 @@ "actions-confirmation-label": "確認訊息", "actions-confirmation-message": "提供描述性提示以確認或取消動作。", "footer-add-annotation": "新增注釋", + "footer-apply-series-as-filter": "", + "footer-apply-series-as-inverse-filter": "", "footer-click-to-action": "點選以{{actionTitle}}", "footer-click-to-navigate": "點選以開啟「{{linkTitle}}」", "footer-filter-for-value": "篩選值「{{value}}」", From 16f5eab78660ca172d14f2e83b4446048d76a2ac Mon Sep 17 00:00:00 2001 From: Charandas <542168+charandas@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:37:10 -0800 Subject: [PATCH 003/172] MTFF: Allow viewers access in cloud (#114607) * Reapply "K8s: read resource configs from API Enablement for API Builders" (#114475) This reverts commit 4130bd9cd300ec7ce0fb492b3a7229968c9167b4. * revert part that broke things * FF service changes are gonna come later * MTFF: allow viewers access to MTFF by enforcing runtime_config for custom routes * unused var * removed now * pass the test, include defaults * revert sample.ini change --- pkg/registry/apis/ofrep/register.go | 4 - pkg/services/apiserver/config.go | 10 ++ pkg/tests/apis/config_test.go | 144 +++++++++++++++++++++++ pkg/tests/apis/features/features_test.go | 6 +- pkg/tests/apis/helper.go | 7 +- pkg/tests/apis/playlist/playlist_test.go | 3 +- pkg/tests/apis/plugins/discovery_test.go | 3 +- pkg/tests/testinfra/testinfra.go | 16 +++ 8 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 pkg/tests/apis/config_test.go diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index cd6f8f0eb29..d6a443cfee7 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -64,10 +64,6 @@ func NewAPIBuilder(providerType string, url *url.URL, insecure bool, caFile stri } func RegisterAPIService(apiregistration builder.APIRegistrar, cfg *setting.Cfg) (*APIBuilder, error) { - if !cfg.OpenFeature.APIEnabled { - return nil, nil - } - var staticEvaluator featuremgmt.StaticFlagEvaluator // No static evaluator needed for non-static provider var err error if cfg.OpenFeature.ProviderType == setting.StaticProviderType { diff --git a/pkg/services/apiserver/config.go b/pkg/services/apiserver/config.go index a0a9f007bcd..499cdb4df6f 100644 --- a/pkg/services/apiserver/config.go +++ b/pkg/services/apiserver/config.go @@ -5,6 +5,7 @@ import ( "net" "path/filepath" "strconv" + "strings" "github.com/grafana/grafana/pkg/services/apiserver/options" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -40,6 +41,15 @@ func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver") runtimeConfig := apiserverCfg.Key("runtime_config").String() + runtimeConfigSplit := strings.Split(runtimeConfig, ",") + + // TODO: temporary fix to allow disabling local features service and still being able to use its authz handler + if !cfg.OpenFeature.APIEnabled { + runtimeConfigSplit = append(runtimeConfigSplit, "features.grafana.app/v0alpha1=false") + } + + runtimeConfig = strings.Join(runtimeConfigSplit, ",") + if runtimeConfig != "" { if err := o.APIEnablementOptions.RuntimeConfig.Set(runtimeConfig); err != nil { return fmt.Errorf("failed to set runtime config: %w", err) diff --git a/pkg/tests/apis/config_test.go b/pkg/tests/apis/config_test.go new file mode 100644 index 00000000000..bf00affdfff --- /dev/null +++ b/pkg/tests/apis/config_test.go @@ -0,0 +1,144 @@ +package apis + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/util/testutil" +) + +const pluginsDiscoveryJSON = `[ +{ + "version": "v0alpha1", + "freshness": "Current", + "resources": [ + { + "resource": "pluginmetas", + "responseKind": { + "group": "", + "kind": "PluginMeta", + "version": "" + }, + "scope": "Namespaced", + "singularResource": "pluginmeta", + "subresources": [ + { + "responseKind": { + "group": "", + "kind": "PluginMeta", + "version": "" + }, + "subresource": "status", + "verbs": [ + "get", + "patch", + "update" + ] + } + ], + "verbs": [ + "get", + "list" + ] + }, + { + "resource": "plugins", + "responseKind": { + "group": "", + "kind": "Plugin", + "version": "" + }, + "scope": "Namespaced", + "singularResource": "plugins", + "subresources": [ + { + "responseKind": { + "group": "", + "kind": "Plugin", + "version": "" + }, + "subresource": "status", + "verbs": [ + "get", + "patch", + "update" + ] + } + ], + "verbs": [ + "create", + "delete", + "deletecollection", + "get", + "list", + "patch", + "update", + "watch" + ] + } + ] +} +]` + +func setupHelper(t *testing.T, openFeatureAPIEnabled bool) *K8sTestHelper { + t.Helper() + helper := NewK8sTestHelper(t, testinfra.GrafanaOpts{ + AppModeProduction: true, + DisableAnonymous: true, + APIServerRuntimeConfig: "plugins.grafana.app/v0alpha1=true", + OpenFeatureAPIEnabled: openFeatureAPIEnabled, + }) + t.Cleanup(func() { helper.Shutdown() }) + return helper +} + +func TestIntegrationAPIServerRuntimeConfig(t *testing.T) { + testutil.SkipIntegrationTestInShortMode(t) + + t.Run("discovery with openfeature api enabled", func(t *testing.T) { + helper := setupHelper(t, true) + disco, err := helper.GetGroupVersionInfoJSON("features.grafana.app") + require.NoError(t, err) + require.JSONEq(t, `[ + { + "freshness": "Current", + "resources": [ + { + "resource": "noop", + "responseKind": { + "group": "", + "kind": "Status", + "version": "" + }, + "scope": "Namespaced", + "singularResource": "noop", + "verbs": [ + "get" + ] + } + ], + "version": "v0alpha1" + } + ]`, disco) + + // plugins should still be discoverable + disco, err = helper.GetGroupVersionInfoJSON("plugins.grafana.app") + require.NoError(t, err) + require.JSONEq(t, pluginsDiscoveryJSON, disco) + require.NoError(t, err) + }) + + t.Run("discovery with openfeature api false", func(t *testing.T) { + helper := setupHelper(t, false) + _, err := helper.GetGroupVersionInfoJSON("features.grafana.app") + require.Error(t, err, "expected error when openfeature api is disabled") + + // plugins should still be discoverable + disco, err := helper.GetGroupVersionInfoJSON("plugins.grafana.app") + require.NoError(t, err) + require.JSONEq(t, pluginsDiscoveryJSON, disco) + require.NoError(t, err) + }) +} diff --git a/pkg/tests/apis/features/features_test.go b/pkg/tests/apis/features/features_test.go index 388268aad19..ab7bfea64d4 100644 --- a/pkg/tests/apis/features/features_test.go +++ b/pkg/tests/apis/features/features_test.go @@ -22,9 +22,11 @@ func TestIntegrationFeatures(t *testing.T) { // Enable a random flag -- check that it is reported as enabled flag := featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs + // the test below tests using enable_api = true, without that, the runtime_config has been instructed to skip the API helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: true, - DisableAnonymous: false, // allow anon user + OpenFeatureAPIEnabled: true, + AppModeProduction: true, + DisableAnonymous: false, // allow anon user EnableFeatureToggles: []string{ flag, // used in test below }, diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index 35c349184bf..379e3ac9734 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -792,7 +792,7 @@ func (c *K8sTestHelper) NewDiscoveryClient() *discovery.DiscoveryClient { return client } -func (c *K8sTestHelper) GetGroupVersionInfoJSON(group string) string { +func (c *K8sTestHelper) GetGroupVersionInfoJSON(group string) (string, error) { c.t.Helper() disco := c.NewDiscoveryClient() @@ -823,12 +823,11 @@ func (c *K8sTestHelper) GetGroupVersionInfoJSON(group string) string { if item.Metadata.Name == group { v, err := json.MarshalIndent(item.Versions, "", " ") require.NoError(c.t, err) - return string(v) + return string(v), nil } } - require.Failf(c.t, "could not find discovery info for: %s", group) - return "" + return "", goerrors.New("could not find discovery info for: " + group) } func (c *K8sTestHelper) CreateDS(cmd *datasources.AddDataSourceCommand) *datasources.DataSource { diff --git a/pkg/tests/apis/playlist/playlist_test.go b/pkg/tests/apis/playlist/playlist_test.go index 49cc20d9f4c..057452eb7f9 100644 --- a/pkg/tests/apis/playlist/playlist_test.go +++ b/pkg/tests/apis/playlist/playlist_test.go @@ -50,7 +50,8 @@ func TestIntegrationPlaylist(t *testing.T) { })) // The accepted verbs will change when dual write is enabled - disco := h.GetGroupVersionInfoJSON("playlist.grafana.app") + disco, err := h.GetGroupVersionInfoJSON("playlist.grafana.app") + require.NoError(t, err) // t.Logf("%s", disco) require.JSONEq(t, `[ { diff --git a/pkg/tests/apis/plugins/discovery_test.go b/pkg/tests/apis/plugins/discovery_test.go index 8f0d0cc5ef5..a6e507a630b 100644 --- a/pkg/tests/apis/plugins/discovery_test.go +++ b/pkg/tests/apis/plugins/discovery_test.go @@ -13,7 +13,8 @@ func TestIntegrationPluginsIntegrationDiscovery(t *testing.T) { t.Run("discovery", func(t *testing.T) { helper := setupHelper(t) - disco := helper.GetGroupVersionInfoJSON("plugins.grafana.app") + disco, err := helper.GetGroupVersionInfoJSON("plugins.grafana.app") + require.NoError(t, err) require.JSONEq(t, `[ { "version": "v0alpha1", diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index cb38d0cd768..e9795f945ab 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -316,6 +316,21 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) { _, err = serverSect.NewKey("static_root_path", publicDir) require.NoError(t, err) + openFeatureSect, err := cfg.NewSection("feature_toggles.openfeature") + require.NoError(t, err) + _, err = openFeatureSect.NewKey("enable_api", strconv.FormatBool(opts.OpenFeatureAPIEnabled)) + require.NoError(t, err) + if !opts.OpenFeatureAPIEnabled { + _, err = openFeatureSect.NewKey("provider", "static") // in practice, APIEnabled being false goes with goff type, but trying to make tests work + require.NoError(t, err) + _, err = openFeatureSect.NewKey("targetingKey", "grafana") + require.NoError(t, err) + + // so staticFlags can be provided to static provider + _, err := cfg.NewSection("feature_toggles") + require.NoError(t, err) + } + anonSect, err := cfg.NewSection("auth.anonymous") require.NoError(t, err) _, err = anonSect.NewKey("enabled", "true") @@ -654,6 +669,7 @@ type GrafanaOpts struct { DisableDBCleanup bool DisableDataMigrations bool SecretsManagerEnableDBMigrations bool + OpenFeatureAPIEnabled bool // Allow creating grafana dir beforehand Dir string From 8eb25a01646693436b09d4fa90521320a40d02c0 Mon Sep 17 00:00:00 2001 From: Gareth Date: Tue, 9 Dec 2025 17:18:06 +0900 Subject: [PATCH 004/172] OpenTSDB: Support all query options in the backend (#114822) * update backend to support all query options * update backend tests * move formatDownsampleInterval to utils --- pkg/tsdb/opentsdb/opentsdb.go | 31 ++- pkg/tsdb/opentsdb/opentsdb_test.go | 201 ++++++++++++++++++ pkg/tsdb/opentsdb/types.go | 5 +- pkg/tsdb/opentsdb/utils.go | 31 +++ .../plugins/datasource/opentsdb/datasource.ts | 24 +++ 5 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 pkg/tsdb/opentsdb/utils.go diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 8b34b21cbaf..d00242f6432 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -62,6 +62,7 @@ type QueryModel struct { IsCounter bool `json:"isCounter"` CounterMax string `json:"counterMax"` CounterResetValue string `json:"counterResetValue"` + ExplicitTags bool `json:"explicitTags"` } func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc { @@ -236,8 +237,19 @@ func createInitialFrame(val OpenTsdbCommon, length int, refID string) *data.Fram labels[label] = value } + tagKeys := make([]string, 0, len(val.Tags)+len(val.AggregateTags)) + for tagKey := range val.Tags { + tagKeys = append(tagKeys, tagKey) + } + sort.Strings(tagKeys) + tagKeys = append(tagKeys, val.AggregateTags...) + frame := data.NewFrameOfFieldTypes(val.Metric, length, data.FieldTypeTime, data.FieldTypeFloat64) - frame.Meta = &data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}} + frame.Meta = &data.FrameMeta{ + Type: data.FrameTypeTimeSeriesMulti, + TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": tagKeys}, + } frame.RefID = refID timeField := frame.Fields[0] timeField.Name = data.TimeSeriesTimeFieldName @@ -355,10 +367,19 @@ func (s *Service) buildMetric(query backend.DataQuery) map[string]any { if !model.DisableDownsampling { downsampleInterval := model.DownsampleInterval if downsampleInterval == "" { - downsampleInterval = "1m" // default value for blank + if ms := query.Interval.Milliseconds(); ms > 0 { + downsampleInterval = FormatDownsampleInterval(ms) + } else { + downsampleInterval = "1m" + } + } else if strings.Contains(downsampleInterval, ".") && strings.HasSuffix(downsampleInterval, "s") { + if val, err := strconv.ParseFloat(strings.TrimSuffix(downsampleInterval, "s"), 64); err == nil { + downsampleInterval = strconv.FormatInt(int64(val*1000), 10) + "ms" + } } + downsample := downsampleInterval + "-" + model.DownsampleAggregator - if model.DownsampleFillPolicy != "none" { + if model.DownsampleFillPolicy != "" && model.DownsampleFillPolicy != "none" { metric["downsample"] = downsample + "-" + model.DownsampleFillPolicy } else { metric["downsample"] = downsample @@ -408,6 +429,10 @@ func (s *Service) buildMetric(query backend.DataQuery) map[string]any { metric["filters"] = model.Filters } + if model.ExplicitTags { + metric["explicitTags"] = true + } + return metric } diff --git a/pkg/tsdb/opentsdb/opentsdb_test.go b/pkg/tsdb/opentsdb/opentsdb_test.go index b959e9efa26..a0150b9e75f 100644 --- a/pkg/tsdb/opentsdb/opentsdb_test.go +++ b/pkg/tsdb/opentsdb/opentsdb_test.go @@ -70,6 +70,164 @@ func TestCheckHealth(t *testing.T) { } } +func TestBuildMetric(t *testing.T) { + service := &Service{} + + t.Run("Metric with no downsampleInterval should use query interval", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "avg", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 30 * time.Second, + } + + metric := service.buildMetric(query) + require.Equal(t, "30s-avg", metric["downsample"], "should use query interval formatted as seconds") + }) + + t.Run("Metric with downsampleInterval converts decimal seconds to milliseconds", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "0.5s", + "downsampleAggregator": "avg", + "downsampleFillPolicy": "none" + }`, + ), + } + + metric := service.buildMetric(query) + require.Equal(t, "500ms-avg", metric["downsample"], "should convert 0.5s to 500ms") + }) + + t.Run("Metric with no downsampleInterval uses milliseconds for sub-second query interval", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "avg", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 500 * time.Millisecond, + } + + metric := service.buildMetric(query) + require.Equal(t, "500ms-avg", metric["downsample"], "should use query interval formatted as milliseconds") + }) + + t.Run("Metric with no downsampleInterval uses minutes for longer intervals", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "sum", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 5 * time.Minute, + } + + metric := service.buildMetric(query) + require.Equal(t, "5m-sum", metric["downsample"], "should use query interval formatted as minutes") + }) + + t.Run("Metric with no downsampleInterval uses hours for multi-hour intervals", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "max", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 2 * time.Hour, + } + + metric := service.buildMetric(query) + require.Equal(t, "2h-max", metric["downsample"], "should use query interval formatted as hours") + }) + + t.Run("Metric with no downsampleInterval uses days for multi-day intervals", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": false, + "downsampleInterval": "", + "downsampleAggregator": "min", + "downsampleFillPolicy": "none" + }`, + ), + Interval: 48 * time.Hour, + } + + metric := service.buildMetric(query) + require.Equal(t, "2d-min", metric["downsample"], "should use query interval formatted as days") + }) + + t.Run("Build metric with explicitTags enabled", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": true, + "explicitTags": true, + "tags": { + "host": "server01" + } + }`, + ), + } + + metric := service.buildMetric(query) + require.True(t, metric["explicitTags"].(bool), "explicitTags should be true") + + metricTags := metric["tags"].(map[string]any) + require.Equal(t, "server01", metricTags["host"]) + }) + + t.Run("Build metric with explicitTags disabled does not include explicitTags", func(t *testing.T) { + query := backend.DataQuery{ + JSON: []byte(` + { + "metric": "cpu.average.percent", + "aggregator": "avg", + "disableDownsampling": true, + "explicitTags": false, + "tags": { + "host": "server01" + } + }`, + ), + } + + metric := service.buildMetric(query) + require.Nil(t, metric["explicitTags"], "explicitTags should not be present when false") + }) +} + func TestOpenTsdbExecutor(t *testing.T) { service := &Service{} @@ -119,6 +277,7 @@ func TestOpenTsdbExecutor(t *testing.T) { testFrame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": []string{"app", "env"}}, } testFrame.RefID = "A" tsdbVersion := float32(4) @@ -160,6 +319,7 @@ func TestOpenTsdbExecutor(t *testing.T) { testFrame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": []string{"app", "env"}}, } testFrame.RefID = "A" tsdbVersion := float32(3) @@ -232,6 +392,7 @@ func TestOpenTsdbExecutor(t *testing.T) { testFrame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": []string{"app", "env"}}, } testFrame.RefID = "A" tsdbVersion := float32(3) @@ -275,6 +436,7 @@ func TestOpenTsdbExecutor(t *testing.T) { testFrame.Meta = &data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, + Custom: map[string]any{"tagKeys": []string{"app", "env"}}, } testFrame.RefID = myRefid @@ -290,6 +452,45 @@ func TestOpenTsdbExecutor(t *testing.T) { } }) + t.Run("tagKeys are returned sorted alphabetically in frame metadata", func(t *testing.T) { + response := ` + [ + { + "metric": "cpu.usage", + "dps": [ + [1405544146, 75.5] + ], + "tags" : { + "zone": "us-east-1", + "host": "server01", + "app": "api", + "env": "production" + } + } + ]` + + tsdbVersion := float32(4) + + resp := http.Response{Body: io.NopCloser(strings.NewReader(response))} + resp.StatusCode = 200 + result, err := service.parseResponse(logger, &resp, "A", tsdbVersion) + require.NoError(t, err) + + frame := result.Responses["A"].Frames[0] + require.NotNil(t, frame.Meta, "frame metadata should not be nil") + require.NotNil(t, frame.Meta.Custom, "frame custom metadata should not be nil") + + customMeta, ok := frame.Meta.Custom.(map[string]any) + require.True(t, ok, "custom metadata should be a map") + + tagKeys, ok := customMeta["tagKeys"].([]string) + require.True(t, ok, "tagKeys should be present and be a string slice") + require.Len(t, tagKeys, 4, "should have 4 tag keys") + + expectedTagKeys := []string{"app", "env", "host", "zone"} + require.Equal(t, expectedTagKeys, tagKeys, "tagKeys should be sorted alphabetically") + }) + t.Run("Build metric with downsampling enabled", func(t *testing.T) { query := backend.DataQuery{ JSON: []byte(` diff --git a/pkg/tsdb/opentsdb/types.go b/pkg/tsdb/opentsdb/types.go index 19d2ba75197..89aed49baa8 100644 --- a/pkg/tsdb/opentsdb/types.go +++ b/pkg/tsdb/opentsdb/types.go @@ -7,8 +7,9 @@ type OpenTsdbQuery struct { } type OpenTsdbCommon struct { - Metric string `json:"metric"` - Tags map[string]string `json:"tags"` + Metric string `json:"metric"` + Tags map[string]string `json:"tags"` + AggregateTags []string `json:"aggregateTags"` } type OpenTsdbResponse struct { diff --git a/pkg/tsdb/opentsdb/utils.go b/pkg/tsdb/opentsdb/utils.go new file mode 100644 index 00000000000..ae57b3e787a --- /dev/null +++ b/pkg/tsdb/opentsdb/utils.go @@ -0,0 +1,31 @@ +package opentsdb + +import ( + "strconv" + "time" +) + +func FormatDownsampleInterval(ms int64) string { + duration := time.Duration(ms) * time.Millisecond + + seconds := int64(duration / time.Second) + if seconds < 60 { + if seconds < 1 { + return strconv.FormatInt(ms, 10) + "ms" + } + return strconv.FormatInt(seconds, 10) + "s" + } + + minutes := int64(duration / time.Minute) + if minutes < 60 { + return strconv.FormatInt(minutes, 10) + "m" + } + + hours := int64(duration / time.Hour) + if hours < 24 { + return strconv.FormatInt(hours, 10) + "h" + } + + days := int64(duration / (24 * time.Hour)) + return strconv.FormatInt(days, 10) + "d" +} diff --git a/public/app/plugins/datasource/opentsdb/datasource.ts b/public/app/plugins/datasource/opentsdb/datasource.ts index 14c24b5c34d..1413daacfc2 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.ts +++ b/public/app/plugins/datasource/opentsdb/datasource.ts @@ -18,6 +18,7 @@ import { catchError, map } from 'rxjs/operators'; import { AnnotationEvent, + DataFrame, DataQueryRequest, DataQueryResponse, dateMath, @@ -78,6 +79,20 @@ export default class OpenTsDatasource extends DataSourceWithBackend): Observable { + if (config.featureToggles.opentsdbBackendMigration) { + const hasValidTargets = options.targets.some((target) => target.metric && !target.hide); + if (!hasValidTargets) { + return of({ data: [] }); + } + + return super.query(options).pipe( + map((response) => { + this._saveTagKeysFromFrames(response.data); + return response; + }) + ); + } + // migrate annotations if (options.targets.some((target: OpenTsdbQuery) => target.fromAnnotations)) { const streams: Array> = []; @@ -265,6 +280,15 @@ export default class OpenTsDatasource extends DataSourceWithBackend { From de42ff2f759aff64fa1a53bf2ba29acce1871cca Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Tue, 9 Dec 2025 10:24:05 +0200 Subject: [PATCH 005/172] Dashboards: Fix versions tab not showing in dashboard settings after making dashboard editable (#114963) * fix showing versions tab on dashboard settings after making dashboard editable * Update public/app/features/dashboard-scene/scene/NavToolbarActions.tsx Co-authored-by: Marc M. <146180665+grafakus@users.noreply.github.com> * Update public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx Co-authored-by: Marc M. <146180665+grafakus@users.noreply.github.com> --------- Co-authored-by: Marc M. <146180665+grafakus@users.noreply.github.com> --- .../scene/NavToolbarActions.test.tsx | 14 +++++++++++++- .../dashboard-scene/scene/NavToolbarActions.tsx | 2 +- .../actions/MakeDashboardEditableButton.test.tsx | 16 ++++++++++++++-- .../actions/MakeDashboardEditableButton.tsx | 2 +- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index e276bf80e46..d40fcfea59f 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -186,6 +186,17 @@ describe('NavToolbarActions', () => { }); }); }); + + describe('where dashboard is not editable', () => { + it('should set dashboard to editable on make editable button press', async () => { + const { dashboard } = setup({}, true); + await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton)); + + expect(dashboard.state.editable).toBe(true); + expect(dashboard.state.meta.canEdit).toBe(true); + expect(dashboard.state.meta.canSave).toBe(true); + }); + }); }); describe('Given new sharing button', () => { @@ -214,7 +225,7 @@ describe('NavToolbarActions', () => { }); }); -function setup(meta?: DashboardMeta) { +function setup(meta?: DashboardMeta, editable?: boolean) { const dashboard = new DashboardScene({ $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), meta: { @@ -229,6 +240,7 @@ function setup(meta?: DashboardMeta) { ...meta, }, title: 'hello', + editable: editable || true, uid: 'dash-1', body: DefaultGridLayoutManager.fromVizPanels([ new VizPanel({ diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 959913c8f8f..eb063b47090 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -351,7 +351,7 @@ export function ToolbarActions({ dashboard }: Props) { onClick={() => { trackDashboardSceneEditButtonClicked(dashboard.state.uid); dashboard.onEnterEditMode(); - dashboard.setState({ editable: true, meta: { ...meta, canEdit: true } }); + dashboard.setState({ meta: { ...meta, canEdit: true, canSave: true } }); }} tooltip={t('dashboard.toolbar.enter-edit-mode.tooltip', 'This dashboard was marked as read only')} key="edit" diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.test.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.test.tsx index 87788a2b187..02a635cf576 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.test.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.test.tsx @@ -34,10 +34,11 @@ setPluginImportUtils({ getPanelPluginFromCache: (id: string) => undefined, }); -export function buildTestScene(isEditing = false) { +export function buildTestScene(isEditing?: boolean, editable?: boolean) { const testScene = new DashboardScene({ $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), - isEditing: isEditing, + isEditing: isEditing || false, + editable: editable || true, body: new DefaultGridLayoutManager({ grid: new SceneGridLayout({ children: [new DashboardGridItem({ body: new VizPanel({ key: 'panel-1', pluginId: 'text' }) })], @@ -76,4 +77,15 @@ describe('MakeDashboardEditableButton', () => { expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false }); }); }); + + it('should set state correctly', async () => { + const scene = buildTestScene(false, false); + + render(); + await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton)); + + expect(scene.state.editable).toBe(true); + expect(scene.state.meta.canEdit).toBe(true); + expect(scene.state.meta.canSave).toBe(true); + }); }); diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx index 3e6386d46e7..b93aad22034 100644 --- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx +++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/MakeDashboardEditableButton.tsx @@ -13,7 +13,7 @@ export const MakeDashboardEditableButton = ({ dashboard }: ToolbarActionProps) = onClick={() => { trackDashboardSceneEditButtonClicked(dashboard.state.uid); dashboard.onEnterEditMode(); - dashboard.setState({ editable: true, meta: { ...dashboard.state.meta, canEdit: true } }); + dashboard.setState({ meta: { ...dashboard.state.meta, canEdit: true, canSave: true } }); }} tooltip={t('dashboard.toolbar.new.enter-edit-mode.tooltip', 'This dashboard was marked as read only')} variant="secondary" From ca342afb255271491fb454973164d38a94e83fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 9 Dec 2025 09:59:40 +0100 Subject: [PATCH 006/172] AppChrome: Add proper menu icon for menu, logo icon becomes home (#114713) * AppChrome: Add proper menu icon for menu, logo icon becomes home * Update * Update * fix merge issue * Aligning icons * Simplify styling and fix issues * fixes * style fix * Fixed unit test * review updates * update * Update * Remove feature highlight * fix lint * remove unused parts --- .../prometheus-variable-editor.spec.ts | 2 +- .../ToolbarButton/ToolbarButton.tsx | 2 +- .../components/AppChrome/AppChromeService.tsx | 2 +- .../AppChrome/MegaMenu/FeatureHighlight.tsx | 34 ----------- .../AppChrome/MegaMenu/MegaMenu.tsx | 8 +-- .../AppChrome/MegaMenu/MegaMenuHeader.tsx | 39 ++++--------- .../AppChrome/MegaMenu/MegaMenuItem.tsx | 26 +++------ .../AppChrome/MegaMenu/MegaMenuItemText.tsx | 10 ++-- .../AppChrome/TopBar/SingleTopBar.tsx | 6 +- .../app/core/components/Branding/Branding.tsx | 56 ++++++++++++++++++- .../core/components/Breadcrumbs/utils.test.ts | 10 +--- .../app/core/components/Breadcrumbs/utils.ts | 10 +--- public/app/core/components/Page/Page.test.tsx | 1 + .../app/core/components/Page/usePageTitle.ts | 10 +++- .../containers/DashboardPage.test.tsx | 6 +- 15 files changed, 100 insertions(+), 122 deletions(-) delete mode 100644 public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx diff --git a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts index 16a722854aa..82b293da581 100644 --- a/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts +++ b/e2e-playwright/various-suite/prometheus-variable-editor.spec.ts @@ -95,7 +95,7 @@ test.describe( await createNewPanelButton.click(); // Close the data source picker modal - const closeButton = page.getByRole('button', { name: 'Close menu' }); + const closeButton = page.getByRole('button', { name: 'Close', exact: true }); await closeButton.click({ force: true }); // Select prom data source from the data source list diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx index c1f99b0db93..2193103a90f 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx @@ -206,7 +206,7 @@ const getStyles = (theme: GrafanaTheme2) => { background: 'transparent', border: `1px solid transparent`, - '&:hover, &:focus': { + '&:hover': { color: theme.colors.text.primary, background: theme.colors.action.hover, }, diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index f3ab1b63c1b..e340e8276b8 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -119,7 +119,7 @@ export class AppChromeService { }; private getUpdatedHistory(newState: AppChromeState): HistoryEntry[] { - const breadcrumbs = buildBreadcrumbs(newState.sectionNav.node, newState.pageNav, { text: 'Home', url: '/' }, true); + const breadcrumbs = buildBreadcrumbs(newState.sectionNav.node, newState.pageNav, { text: 'Home', url: '/' }); const newPageNav = newState.pageNav || newState.sectionNav.node; let entries = store.getObject(HISTORY_LOCAL_STORAGE_KEY, []); diff --git a/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx b/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx deleted file mode 100644 index ca660e8750c..00000000000 --- a/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { css } from '@emotion/css'; -import type { JSX } from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; - -export interface Props { - children: JSX.Element; -} - -export const FeatureHighlight = ({ children }: Props): JSX.Element => { - const styles = useStyles2(getStyles); - return ( - <> - {children} - - - ); -}; - -const getStyles = (theme: GrafanaTheme2) => { - return { - highlight: css({ - backgroundColor: theme.colors.success.main, - borderRadius: theme.shape.radius.circle, - width: '6px', - height: '6px', - display: 'inline-block;', - position: 'absolute', - top: '50%', - transform: 'translateY(-50%)', - }), - }; -}; diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index dead4bd810e..f4ec7598fa2 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -62,10 +62,6 @@ export const MegaMenu = memo( const activeItem = getActiveItem(navItems, state.sectionNav.node, location.pathname); - const handleMegaMenu = () => { - chrome.setMegaMenuOpen(!state.megaMenuOpen); - }; - const handleDockedMenu = () => { chrome.setMegaMenuDocked(!state.megaMenuDocked); if (state.megaMenuDocked) { @@ -108,7 +104,7 @@ export const MegaMenu = memo( return (
- +