From 408ac3086d753531344a5d744695de8582ceef2c Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:40:19 +0000 Subject: [PATCH 001/101] I18n: Download translations from Crowdin (#108493) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../src/locales/cs-CZ/grafana-prometheus.json | 104 +++++ .../src/locales/de-DE/grafana-prometheus.json | 104 +++++ .../src/locales/es-ES/grafana-prometheus.json | 104 +++++ .../src/locales/fr-FR/grafana-prometheus.json | 104 +++++ .../src/locales/hu-HU/grafana-prometheus.json | 104 +++++ .../src/locales/id-ID/grafana-prometheus.json | 104 +++++ .../src/locales/it-IT/grafana-prometheus.json | 104 +++++ .../src/locales/ja-JP/grafana-prometheus.json | 104 +++++ .../src/locales/ko-KR/grafana-prometheus.json | 104 +++++ .../src/locales/nl-NL/grafana-prometheus.json | 104 +++++ .../src/locales/pl-PL/grafana-prometheus.json | 104 +++++ .../src/locales/pt-BR/grafana-prometheus.json | 104 +++++ .../src/locales/pt-PT/grafana-prometheus.json | 104 +++++ .../src/locales/ru-RU/grafana-prometheus.json | 104 +++++ .../src/locales/sv-SE/grafana-prometheus.json | 104 +++++ .../src/locales/tr-TR/grafana-prometheus.json | 104 +++++ .../locales/zh-Hans/grafana-prometheus.json | 104 +++++ .../locales/zh-Hant/grafana-prometheus.json | 104 +++++ public/locales/cs-CZ/grafana.json | 429 +++++++++++++++++- public/locales/de-DE/grafana.json | 423 ++++++++++++++++- public/locales/es-ES/grafana.json | 423 ++++++++++++++++- public/locales/fr-FR/grafana.json | 423 ++++++++++++++++- public/locales/hu-HU/grafana.json | 423 ++++++++++++++++- public/locales/id-ID/grafana.json | 420 ++++++++++++++++- public/locales/it-IT/grafana.json | 423 ++++++++++++++++- public/locales/ja-JP/grafana.json | 420 ++++++++++++++++- public/locales/ko-KR/grafana.json | 420 ++++++++++++++++- public/locales/nl-NL/grafana.json | 423 ++++++++++++++++- public/locales/pl-PL/grafana.json | 429 +++++++++++++++++- public/locales/pt-BR/grafana.json | 423 ++++++++++++++++- public/locales/pt-PT/grafana.json | 423 ++++++++++++++++- public/locales/ru-RU/grafana.json | 429 +++++++++++++++++- public/locales/sv-SE/grafana.json | 423 ++++++++++++++++- public/locales/tr-TR/grafana.json | 423 ++++++++++++++++- public/locales/zh-Hans/grafana.json | 420 ++++++++++++++++- public/locales/zh-Hant/grafana.json | 420 ++++++++++++++++- 36 files changed, 9380 insertions(+), 109 deletions(-) diff --git a/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json index 8498c0ce6df..57f9cec30c7 100644 --- a/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/cs-CZ/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "Filtrovat názvy metrik podle vyhledávání regulárních výrazů pomocí dalšího volání rozhraní Prometheus API.", @@ -321,6 +418,13 @@ "title-add": "Přidat {{name}}", "title-remove": "Odebrat {{name}}" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "Nastavit dolní limit pro parametr kroku", "aria-label-select-resolution": "Vyberte rozlišení", diff --git a/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json index 5e9aa8833cd..d3c2c2e1743 100644 --- a/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/de-DE/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "Filtern Sie Metriknamen per Regex-Suche und nutzen Sie dabei einen zusätzlichen Aufruf der Prometheus-API.", @@ -319,6 +416,13 @@ "title-add": "{{name}} hinzufügen", "title-remove": "{{name}} entfernen" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "Untergrenze für den Schrittparameter festlegen", "aria-label-select-resolution": "Auflösung auswählen", diff --git a/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json index d4ca0b7b366..fc668327cdb 100644 --- a/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/es-ES/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -319,6 +416,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json index 18813255785..409e95dcf48 100644 --- a/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/fr-FR/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "Filtrez les noms de métriques par recherche d’expression régulière, en utilisant un appel supplémentaire sur l’API Prometheus.", @@ -319,6 +416,13 @@ "title-add": "Ajouter {{name}}", "title-remove": "Supprimer {{name}}" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "Définir la limite inférieure pour le paramètre d’étape", "aria-label-select-resolution": "Sélectionner une résolution", diff --git a/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json index 1dd2ee85be0..22bc5c9c64b 100644 --- a/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/hu-HU/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "A metrikanevek szűrése reguláris kifejezéses kereséssel, a Prometheus API további hívásával.", @@ -319,6 +416,13 @@ "title-add": "{{name}} hozzáadása", "title-remove": "{{name}} eltávolítása" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "Állítsa be a léptékparaméter alsó határát", "aria-label-select-resolution": "Felbontás kiválasztása", diff --git a/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json index 9a7b8f0a52d..f80f5a3d833 100644 --- a/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/id-ID/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "Filter nama metrik dengan pencarian regex, menggunakan panggilan tambahan pada API Prometheus.", @@ -318,6 +415,13 @@ "title-add": "Tambahkan {{name}}", "title-remove": "Hapus {{name}}" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "Tetapkan batas bawah untuk parameter langkah", "aria-label-select-resolution": "Pilih resolusi", diff --git a/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json index 44db96a498b..443fd697bad 100644 --- a/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/it-IT/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -319,6 +416,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json index 618b313fa90..962ded0d7be 100644 --- a/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ja-JP/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "Prometheus APIの追加呼び出しを使用して、正規表現検索でメトリック名をフィルタリングします。", @@ -318,6 +415,13 @@ "title-add": "{{name}}を追加", "title-remove": "{{name}}を削除" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "ステップパラメーターの下限を設定", "aria-label-select-resolution": "解像度を選択", diff --git a/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json index 26c7b27eb2f..0fdb0e6d527 100644 --- a/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ko-KR/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -318,6 +415,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json index d6bbd899aa2..51074f0b25d 100644 --- a/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/nl-NL/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "Filter metrische namen op regex-zoekopdracht, met behulp van een extra oproep op de Prometheus-API.", @@ -319,6 +416,13 @@ "title-add": "{{name}} toevoegen", "title-remove": "{{name}} verwijderen" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "Ondergrens instellen voor de stapparameter", "aria-label-select-resolution": "Resolutie selecteren", diff --git a/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json index 38aacf94629..25ef725f484 100644 --- a/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pl-PL/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -321,6 +418,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json index 775cb9f3cc3..74e177550c6 100644 --- a/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pt-BR/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -319,6 +416,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json index 5d8aa3f0402..93d395b3f57 100644 --- a/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/pt-PT/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -319,6 +416,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json index e593d2333aa..265b74d6ac3 100644 --- a/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/ru-RU/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -321,6 +418,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json index 7e50b6aa9ab..7e57336aaca 100644 --- a/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/sv-SE/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -319,6 +416,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json index 46d4ce11257..b834bace3db 100644 --- a/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/tr-TR/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "", @@ -319,6 +416,13 @@ "title-add": "", "title-remove": "" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "", "aria-label-select-resolution": "", diff --git a/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json index 9bb70f50992..242003931cd 100644 --- a/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/zh-Hans/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "使用 Prometheus API 上的其他调用,通过正则表达式搜索筛选指标名称。", @@ -318,6 +415,13 @@ "title-add": "添加{{name}}", "title-remove": "移除{{name}}" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "设置步长参数的下限", "aria-label-select-resolution": "选择分辨率", diff --git a/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json index 80a90677692..81bdc65f727 100644 --- a/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/zh-Hant/grafana-prometheus.json @@ -209,6 +209,103 @@ "label-verbose": "" } }, + "promql": { + "getAggregationOptions": { + "documentation-avg": "", + "documentation-bottomk": "", + "documentation-count": "", + "documentation-count-values": "", + "documentation-group": "", + "documentation-max": "", + "documentation-min": "", + "documentation-quantile": "", + "documentation-stddev": "", + "documentation-stdvar": "", + "documentation-sum": "", + "documentation-topk": "" + }, + "getFunctions": { + "documentation-abs": "", + "documentation-absent": "", + "documentation-absent-over-time": "", + "documentation-avg-over-time": "", + "documentation-ceil": "", + "documentation-changes": "", + "documentation-clamp": "", + "documentation-clamp-max": "", + "documentation-clamp-min": "", + "documentation-count-over-time": "", + "documentation-count-scalar": "", + "documentation-day-of-month": "", + "documentation-day-of-week": "", + "documentation-day-of-year": "", + "documentation-days-in-month": "", + "documentation-deg": "", + "documentation-delta": "", + "documentation-deriv": "", + "documentation-double-exponential-smoothing": "", + "documentation-drop-common-labels": "", + "documentation-exp": "", + "documentation-floor": "", + "documentation-histogram-avg": "", + "documentation-histogram-count": "", + "documentation-histogram-fraction": "", + "documentation-histogram-quantile": "", + "documentation-histogram-stddev": "", + "documentation-histogram-stdvar": "", + "documentation-histogram-sum": "", + "documentation-holt-winters": "", + "documentation-hour": "", + "documentation-idelta": "", + "documentation-increase": "", + "documentation-info": "", + "documentation-irate": "", + "documentation-label-join": "", + "documentation-label-replace": "", + "documentation-last-over-time": "", + "documentation-ln": "", + "documentation-log10": "", + "documentation-log2": "", + "documentation-max-over-time": "", + "documentation-min-over-time": "", + "documentation-minute": "", + "documentation-month": "", + "documentation-pi": "", + "documentation-predict-linear": "", + "documentation-present-over-time": "", + "documentation-quantile-over-time": "", + "documentation-rad": "", + "documentation-rate": "", + "documentation-resets": "", + "documentation-round": "", + "documentation-scalar": "", + "documentation-sgn": "", + "documentation-sort": "", + "documentation-sort-desc": "", + "documentation-sqrt": "", + "documentation-stddev-over-time": "", + "documentation-stdvar-over-time": "", + "documentation-sum-over-time": "", + "documentation-time": "", + "documentation-timestamp": "", + "documentation-vector": "", + "documentation-year": "" + }, + "getTrigonometricFunctions": { + "documentation-acos": "", + "documentation-acosh": "", + "documentation-asin": "", + "documentation-asinh": "", + "documentation-atan": "", + "documentation-atanh": "", + "documentation-cos": "", + "documentation-cosh": "", + "documentation-sin": "", + "documentation-sinh": "", + "documentation-tan": "", + "documentation-tanh": "" + } + }, "querybuilder": { "additional-settings": { "content-filter-metric-names-regex-search-using": "透過正規表達式搜尋篩選指標名稱,並使用額外的 Prometheus API 呼叫。", @@ -318,6 +415,13 @@ "title-add": "新增{{name}}", "title-remove": "移除{{name}}" }, + "operation-utils": { + "getAggregationExplainer": { + "label-by": "", + "label-default": "", + "label-without": "" + } + }, "prom-query-builder-options": { "aria-label-lower-limit-parameter": "設定步驟參數的下限", "aria-label-select-resolution": "選擇解決方案", diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index af45c9e8572..c114c5f82e6 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -3547,14 +3547,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_few": "", - "api-success_many": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7780,6 +7780,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10646,10 +11049,18 @@ "label-display-name": "Zobrazované jméno", "placeholder-my-repository-connection": "Moje připojení k úložišti", "text-loading-resource-information": "Načítání informací o zdroji…", + "dashboards-count_one": "", + "dashboards-count_few": "", + "dashboards-count_many": "", + "dashboards-count_other": "", "files-count_one": "Počet souborů: {{count}}", "files-count_few": "Počet souborů: {{count}}", "files-count_many": "Počet souborů: {{count}}", - "files-count_other": "Počet souborů: {{count}}" + "files-count_other": "Počet souborů: {{count}}", + "folders-count_one": "", + "folders-count_few": "", + "folders-count_many": "", + "folders-count_other": "" }, "check-repository": { "check": "Kontrola" @@ -11013,6 +11424,11 @@ "label-current-step": "Aktuální krok", "label-pending-step": "Nevyřízený krok" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Upravit konfiguraci", "button-edit": "Upravit", @@ -11023,9 +11439,6 @@ "tooltip-unhealthy-repository": "Nelze stáhnout nezdravé úložiště" }, "synchronize-step": { - "error-no-job-id": "Nepodařilo se spustit úlohu", - "error-no-repository-name": "Nebyl zadán žádný název úložiště", - "error-starting-job": "Chyba při spouštění úlohy", "synchronization-description": "Zahrnout předložení pro každou historickou hodnotu", "synchronization-options": "Možnosti synchronizace" }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 186d6e34398..34931789e34 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Anzeigename", "placeholder-my-repository-connection": "Meine Repository-Verbindung", "text-loading-resource-information": "Ressourceninformationen werden geladen ...", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} Dateien", - "files-count_other": "{{count}} Dateien" + "files-count_other": "{{count}} Dateien", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Überprüfen" @@ -10943,6 +11352,11 @@ "label-current-step": "Aktueller Schritt", "label-pending-step": "Ausstehender Schritt" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Konfiguration bearbeiten", "button-edit": "Bearbeiten", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "Ein fehlerhaftes Repository kann nicht abgerufen werden" }, "synchronize-step": { - "error-no-job-id": "Auftrag konnte nicht gestartet werden", - "error-no-repository-name": "Kein Repository-Name angegeben", - "error-starting-job": "Fehler beim Starten des Auftrags", "synchronization-description": "Commits für jeden historischen Wert erfassen", "synchronization-options": "Synchronisierungsoptionen" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 980ce238cc2..2512bd3db7f 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Nombre que mostrar", "placeholder-my-repository-connection": "Mi conexión de repositorio", "text-loading-resource-information": "Cargando información de recursos...", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} archivos", - "files-count_other": "{{count}} archivos" + "files-count_other": "{{count}} archivos", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Verificar" @@ -10943,6 +11352,11 @@ "label-current-step": "Paso actual", "label-pending-step": "Paso pendiente" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Editar la configuración", "button-edit": "Editar", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "No se puede extraer un repositorio que no está en buen estado" }, "synchronize-step": { - "error-no-job-id": "Error al iniciar el trabajo", - "error-no-repository-name": "No se proporcionó ningún nombre de repositorio", - "error-starting-job": "Error al iniciar el trabajo", "synchronization-description": "Incluir confirmaciones para cada valor histórico", "synchronization-options": "Opciones de sincronización" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index d377cc17eee..b413dec5dff 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Pseudo", "placeholder-my-repository-connection": "Ma connexion au référentiel", "text-loading-resource-information": "Chargement des informations sur les ressources...", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} fichiers", - "files-count_other": "{{count}} fichiers" + "files-count_other": "{{count}} fichiers", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Vérifier" @@ -10943,6 +11352,11 @@ "label-current-step": "Étape actuelle", "label-pending-step": "Étape en attente" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Modifier la configuration", "button-edit": "Modifier", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "Impossible de fusionner un référentiel en mauvais état" }, "synchronize-step": { - "error-no-job-id": "Échec du lancement de la mission", - "error-no-repository-name": "Aucun nom de référentiel fourni", - "error-starting-job": "Erreur lors du lancement de la mission", "synchronization-description": "Inclure les commits pour chaque valeur historique", "synchronization-options": "Options de synchronisation" }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index bb5f21e5b17..4ef8f071309 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Megjelenített név", "placeholder-my-repository-connection": "Saját adattári kapcsolat", "text-loading-resource-information": "Erőforrás-információk betöltése…", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} fájl", - "files-count_other": "{{count}} fájl" + "files-count_other": "{{count}} fájl", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Ellenőrzés" @@ -10943,6 +11352,11 @@ "label-current-step": "Jelenlegi lépés", "label-pending-step": "Függőben lévő lépés" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "A konfiguráció szerkesztése", "button-edit": "Szerkesztés", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "Nem lehet beolvasni egy nem megfelelő állapotú adattárat" }, "synchronize-step": { - "error-no-job-id": "A feladat indítása nem sikerült", - "error-no-repository-name": "Nincs megadva az adattár neve", - "error-starting-job": "Hiba a feladat indításakor", "synchronization-description": "Változtatások belefoglalása minden előzményértékhez", "synchronization-options": "Szinkronizálási beállítások" }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 1612ac766b9..aede0df0441 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -3505,11 +3505,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7711,6 +7714,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10544,7 +10950,9 @@ "label-display-name": "Nama tampilan", "placeholder-my-repository-connection": "Koneksi repositori saya", "text-loading-resource-information": "Memuat informasi sumber daya...", - "files-count_other": "{{count}} file" + "dashboards-count_other": "", + "files-count_other": "{{count}} file", + "folders-count_other": "" }, "check-repository": { "check": "Periksa" @@ -10908,6 +11316,11 @@ "label-current-step": "Langkah saat ini", "label-pending-step": "Langkah tertunda" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Edit konfigurasi", "button-edit": "Edit", @@ -10918,9 +11331,6 @@ "tooltip-unhealthy-repository": "Tidak dapat menerapkan pull pada repositori yang tidak sehat" }, "synchronize-step": { - "error-no-job-id": "Gagal memulai pekerjaan", - "error-no-repository-name": "Tidak ada nama repositori yang disediakan", - "error-starting-job": "Kesalahan saat memulai pekerjaan", "synchronization-description": "Sertakan commit untuk setiap nilai historis", "synchronization-options": "Opsi sinkronisasi" }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 950f8fed8bd..268d227ce05 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Nome visualizzato", "placeholder-my-repository-connection": "La mia connessione al repository", "text-loading-resource-information": "Caricamento delle informazioni sulle risorse in corso...", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} file", - "files-count_other": "{{count}} file" + "files-count_other": "{{count}} file", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Verifica" @@ -10943,6 +11352,11 @@ "label-current-step": "Passaggio corrente", "label-pending-step": "Passaggio in sospeso" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Modifica configurazione", "button-edit": "Modifica", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "Impossibile eseguire il pull di un repository non integro" }, "synchronize-step": { - "error-no-job-id": "Impossibile avviare l'attività", - "error-no-repository-name": "Nessun nome di repository fornito", - "error-starting-job": "Errore durante l'avvio dell'attività", "synchronization-description": "Includi commit per ogni valore storico", "synchronization-options": "Opzioni di sincronizzazione" }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 46d622b4893..1fa5b0e181e 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -3505,11 +3505,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7711,6 +7714,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10544,7 +10950,9 @@ "label-display-name": "表示名", "placeholder-my-repository-connection": "自分のリポジトリ接続", "text-loading-resource-information": "リソース情報読み込み中...", - "files-count_other": "{{count}}個のファイル" + "dashboards-count_other": "", + "files-count_other": "{{count}}個のファイル", + "folders-count_other": "" }, "check-repository": { "check": "確認" @@ -10908,6 +11316,11 @@ "label-current-step": "現在のステップ", "label-pending-step": "保留中のステップ" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "設定を編集", "button-edit": "編集", @@ -10918,9 +11331,6 @@ "tooltip-unhealthy-repository": "問題のあるリポジトリをプルできません" }, "synchronize-step": { - "error-no-job-id": "ジョブを開始できませんでした", - "error-no-repository-name": "リポジトリ名が指定されていません", - "error-starting-job": "ジョブ開始時のエラー", "synchronization-description": "各履歴値のコミットを含める", "synchronization-options": "同期オプション" }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index d9f045c7452..7b75891fede 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -3505,11 +3505,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7711,6 +7714,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10544,7 +10950,9 @@ "label-display-name": "표시 이름", "placeholder-my-repository-connection": "내 리포지토리 연결", "text-loading-resource-information": "리소스 정보 로딩 중...", - "files-count_other": "파일 {{count}}개" + "dashboards-count_other": "", + "files-count_other": "파일 {{count}}개", + "folders-count_other": "" }, "check-repository": { "check": "확인" @@ -10908,6 +11316,11 @@ "label-current-step": "현재 단계", "label-pending-step": "보류 중인 단계" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "구성 편집", "button-edit": "편집", @@ -10918,9 +11331,6 @@ "tooltip-unhealthy-repository": "상태가 좋지 않은 리포지토리를 가져올 수 없습니다" }, "synchronize-step": { - "error-no-job-id": "작업 시작 실패", - "error-no-repository-name": "제공된 리포지토리 이름 없음", - "error-starting-job": "작업 시작 중 오류 발생", "synchronization-description": "각 과거 값에 대한 커밋을 포함합니다", "synchronization-options": "동기화 옵션" }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 6f61412b43a..ee1f03f7e86 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Weergavenaam", "placeholder-my-repository-connection": "Mijn repository-verbinding", "text-loading-resource-information": "Broninformatie laden...", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} bestanden", - "files-count_other": "{{count}} bestanden" + "files-count_other": "{{count}} bestanden", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Controleer" @@ -10943,6 +11352,11 @@ "label-current-step": "Huidige stap", "label-pending-step": "Stap in afwachting" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "De configuratie bewerken", "button-edit": "Bewerken", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "Kan geen ongezonde repository ophalen" }, "synchronize-step": { - "error-no-job-id": "Kan taak niet starten", - "error-no-repository-name": "Geen repository-naam opgegeven", - "error-starting-job": "Er is een fout opgetreden bij het starten van de taak", "synchronization-description": "Commits opnemen voor elke historische waarde", "synchronization-options": "Synchronisatie-opties" }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 943f509dae8..776a6cb5c08 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -3547,14 +3547,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_few": "", - "api-success_many": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7780,6 +7780,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10646,10 +11049,18 @@ "label-display-name": "Nazwa wyświetlana", "placeholder-my-repository-connection": "Połączenie z moim repozytorium", "text-loading-resource-information": "Wczytywanie informacji o zasobach…", + "dashboards-count_one": "", + "dashboards-count_few": "", + "dashboards-count_many": "", + "dashboards-count_other": "", "files-count_one": "{{count}} plik", "files-count_few": "{{count}} pliki", "files-count_many": "{{count}} plików", - "files-count_other": "{{count}} pliku" + "files-count_other": "{{count}} pliku", + "folders-count_one": "", + "folders-count_few": "", + "folders-count_many": "", + "folders-count_other": "" }, "check-repository": { "check": "Sprawdźmy." @@ -11013,6 +11424,11 @@ "label-current-step": "Aktualny krok", "label-pending-step": "Oczekujący krok" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Edytuj konfigurację", "button-edit": "Edytuj", @@ -11023,9 +11439,6 @@ "tooltip-unhealthy-repository": "Nie można pobrać danych z niesprawnego repozytorium" }, "synchronize-step": { - "error-no-job-id": "Nie udało się uruchomić zadania", - "error-no-repository-name": "Nie wprowadzono nazwy repozytorium", - "error-starting-job": "Błąd uruchamiania zadania", "synchronization-description": "Uwzględnij zatwierdzenia dla każdej wartości historycznej", "synchronization-options": "Opcje synchronizacji" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 6ed36e47571..36500301b64 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Nome de exibição", "placeholder-my-repository-connection": "Minha conexão de repositório", "text-loading-resource-information": "Carregando informações de recursos…", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} arquivos", - "files-count_other": "{{count}} arquivos" + "files-count_other": "{{count}} arquivos", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Verificar" @@ -10943,6 +11352,11 @@ "label-current-step": "Etapa atual", "label-pending-step": "Etapa pendente" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Editar a configuração", "button-edit": "Editar", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "Não é possível fazer extração de um repositório instável" }, "synchronize-step": { - "error-no-job-id": "Falha ao iniciar a tarefa", - "error-no-repository-name": "Nenhum nome de repositório foi informado", - "error-starting-job": "Erro ao iniciar a tarefa", "synchronization-description": "Incluir confirmações para cada valor histórico", "synchronization-options": "Opções de sincronização" }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index cdafe26428d..cc5e79646ef 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Nome do visor", "placeholder-my-repository-connection": "A minha ligação de repositório", "text-loading-resource-information": "A carregar informações de recursos...", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} ficheiro", - "files-count_other": "{{count}} ficheiros" + "files-count_other": "{{count}} ficheiros", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Verificar" @@ -10943,6 +11352,11 @@ "label-current-step": "Passo atual", "label-pending-step": "Passo pendente" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Editar a configuração", "button-edit": "Editar", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "Não foi possível obter um repositório que não está em bom estado" }, "synchronize-step": { - "error-no-job-id": "Falha ao iniciar a tarefa", - "error-no-repository-name": "Nenhum nome de repositório fornecido", - "error-starting-job": "Erro ao iniciar a tarefa", "synchronization-description": "Incluir commits para cada valor histórico", "synchronization-options": "Opções de sincronização" }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 4e429994950..a16f5fd8f37 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -3547,14 +3547,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_few": "", - "api-success_many": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7780,6 +7780,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10646,10 +11049,18 @@ "label-display-name": "Отображаемое имя", "placeholder-my-repository-connection": "Мое подключение к репозиторию", "text-loading-resource-information": "Загрузка информации о ресурсе...", + "dashboards-count_one": "", + "dashboards-count_few": "", + "dashboards-count_many": "", + "dashboards-count_other": "", "files-count_one": "{{count}} файл", "files-count_few": "{{count}} файла", "files-count_many": "{{count}} файлов", - "files-count_other": "{{count}} файла" + "files-count_other": "{{count}} файла", + "folders-count_one": "", + "folders-count_few": "", + "folders-count_many": "", + "folders-count_other": "" }, "check-repository": { "check": "Проверить" @@ -11013,6 +11424,11 @@ "label-current-step": "Текущий шаг", "label-pending-step": "Предстоящий шаг" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Редактировать конфигурацию", "button-edit": "Редактировать", @@ -11023,9 +11439,6 @@ "tooltip-unhealthy-repository": "Невозможно внести изменения в неисправный репозиторий" }, "synchronize-step": { - "error-no-job-id": "Не удалось запустить задание", - "error-no-repository-name": "Не указано название репозитория", - "error-starting-job": "Ошибка при запуске задания", "synchronization-description": "Включить фиксации для каждого ранее зафиксированного значения", "synchronization-options": "Параметры синхронизации" }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index af96deb10f8..8478e730cef 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Visningsnamn", "placeholder-my-repository-connection": "Min lagringsplatsanslutning", "text-loading-resource-information": "Läser in resursinformation …", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} filer", - "files-count_other": "{{count}} filer" + "files-count_other": "{{count}} filer", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Kolla" @@ -10943,6 +11352,11 @@ "label-current-step": "Nuvarande steg", "label-pending-step": "Väntande steg" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Redigera konfigurationen", "button-edit": "Redigera", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "Det gick inte att hämta en ohälsosam lagringsplats" }, "synchronize-step": { - "error-no-job-id": "Misslyckades att starta", - "error-no-repository-name": "Inget lagringsplatsnamn angivet", - "error-starting-job": "Fel vid start av jobb", "synchronization-description": "Inkludera åtaganden för varje historiskt värde", "synchronization-options": "Synkroniseringsalternativ" }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 33096ae432d..f8c079e5b78 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -3519,12 +3519,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_one": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7734,6 +7736,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10578,8 +10983,12 @@ "label-display-name": "Görünen ad", "placeholder-my-repository-connection": "Depo bağlantım", "text-loading-resource-information": "Kaynak bilgileri yükleniyor...", + "dashboards-count_one": "", + "dashboards-count_other": "", "files-count_one": "{{count}} dosya", - "files-count_other": "{{count}} dosya" + "files-count_other": "{{count}} dosya", + "folders-count_one": "", + "folders-count_other": "" }, "check-repository": { "check": "Kontrol et" @@ -10943,6 +11352,11 @@ "label-current-step": "Mevcut adım", "label-pending-step": "Bekleyen adım" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "Yapılandırmayı düzenle", "button-edit": "Düzenle", @@ -10953,9 +11367,6 @@ "tooltip-unhealthy-repository": "İyi durumda olmayan bir depo çekilemedi" }, "synchronize-step": { - "error-no-job-id": "İş başlatılamadı", - "error-no-repository-name": "Depo adı sağlanmadı", - "error-starting-job": "İş başlatılırken hata oluştu", "synchronization-description": "Her geçmiş değer için yürütmeyi dahil et", "synchronization-options": "Senkronizasyon seçenekleri" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 951ef6a4554..d87c90a8735 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -3505,11 +3505,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7711,6 +7714,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10544,7 +10950,9 @@ "label-display-name": "显示名称", "placeholder-my-repository-connection": "我的存储库连接", "text-loading-resource-information": "正在加载资源信息...", - "files-count_other": "{{count}} 个文件" + "dashboards-count_other": "", + "files-count_other": "{{count}} 个文件", + "folders-count_other": "" }, "check-repository": { "check": "检查" @@ -10908,6 +11316,11 @@ "label-current-step": "当前步骤", "label-pending-step": "待处理步骤" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "编辑配置", "button-edit": "编辑", @@ -10918,9 +11331,6 @@ "tooltip-unhealthy-repository": "无法拉取状态不良的存储库" }, "synchronize-step": { - "error-no-job-id": "启动作业失败", - "error-no-repository-name": "未提供存储库名称", - "error-starting-job": "启动作业时出错", "synchronization-description": "包括每个历史值的提交", "synchronization-options": "同步选项" }, diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 4b620035699..3e0ec426112 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -3505,11 +3505,14 @@ "button-cancel": "", "button-delete": "", "button-deleting": "", + "button-done": "", "delete-warning": "", "error-path-not-found": "", - "api-success_other": "" + "progress-title": "", + "success-message": "" }, "bulk-move-resources-form": { + "deleting": "", "progress": "" }, "counts": { @@ -7711,6 +7714,409 @@ } } }, + "grafana-data": { + "valueFormats": { + "categories": { + "acceleration": { + "formats": { + "name-feet-sec": "", + "name-g-unit": "", + "name-meters-sec": "" + }, + "name": "" + }, + "angle": { + "formats": { + "name-arc-minutes": "", + "name-arc-seconds": "", + "name-degrees": "", + "name-gradian": "", + "name-radians": "" + }, + "name": "" + }, + "area": { + "formats": { + "name-acres": "", + "name-hectares": "", + "name-square-feet": "", + "name-square-meters": "", + "name-square-miles": "" + }, + "name": "" + }, + "boolean": { + "formats": { + "name-on-off": "", + "name-true-false": "", + "name-yes-no": "" + }, + "name": "" + }, + "computation": { + "formats": { + "name-eflops": "", + "name-flops": "", + "name-gflops": "", + "name-mflops": "", + "name-pflops": "", + "name-tflops": "", + "name-yflops": "", + "name-zflops": "" + }, + "name": "" + }, + "concentration": { + "formats": { + "name-g-m3": "", + "name-g-nm3": "", + "name-mg-dl": "", + "name-mg-m3": "", + "name-mg-nm3": "", + "name-mmol-l": "", + "name-ng-m3": "", + "name-ng-nm3": "", + "name-ppb": "", + "name-ppm": "", + "name-ug-m3": "", + "name-ug-nm3": "" + }, + "name": "" + }, + "currency": { + "formats": { + "name-bgn": "", + "name-brl": "", + "name-btc": "", + "name-chf": "", + "name-czk": "", + "name-dkk": "", + "name-eur": "", + "name-gbp": "", + "name-idr": "", + "name-ils": "", + "name-inr": "", + "name-isk": "", + "name-jpy": "", + "name-krw": "", + "name-mbtc": "", + "name-myr": "", + "name-nok": "", + "name-php": "", + "name-pln": "", + "name-pyg": "", + "name-rub": "", + "name-sek": "", + "name-try": "", + "name-uah": "", + "name-ubtc": "", + "name-usd": "", + "name-uyu": "", + "name-vnd": "", + "name-xpf": "", + "name-zar": "" + }, + "name": "" + }, + "data": { + "formats": { + "name-bits-iec": "", + "name-bits-si": "", + "name-bytes-iec": "", + "name-bytes-si": "", + "name-gibibytes": "", + "name-gigabytes": "", + "name-kibibytes": "", + "name-kilobytes": "", + "name-mebibytes": "", + "name-megabytes": "", + "name-pebibytes": "", + "name-petabytes": "", + "name-tebibytes": "", + "name-terabytes": "" + }, + "name": "" + }, + "data-rate": { + "formats": { + "name-bits-sec-iec": "", + "name-bits-sec-si": "", + "name-bytes-sec-iec": "", + "name-bytes-sec-si": "", + "name-gibibits-sec": "", + "name-gibibytes-sec": "", + "name-gigabits-sec": "", + "name-gigabytes-sec": "", + "name-kibibits-sec": "", + "name-kibibytes-sec": "", + "name-kilobits-sec": "", + "name-kilobytes-sec": "", + "name-mebibits-sec": "", + "name-mebibytes-sec": "", + "name-megabits-sec": "", + "name-megabytes-sec": "", + "name-packets-sec": "", + "name-pebibits-sec": "", + "name-pebibytes-sec": "", + "name-petabits-sec": "", + "name-petabytes-sec": "", + "name-tebibits-sec": "", + "name-tebibytes-sec": "", + "name-terabits-sec": "", + "name-terabytes-sec": "" + }, + "name": "" + }, + "date-time": { + "formats": { + "name-datetime-default": "", + "name-datetime-iso": "", + "name-datetime-iso-no-date": "", + "name-datetime-local": "", + "name-datetime-local-no-date": "", + "name-datetime-us": "", + "name-datetime-us-no-date": "", + "name-from-now": "" + }, + "name": "" + }, + "energy": { + "formats": { + "name-ampere": "", + "name-ampere-hour": "", + "name-decibel-milliwatt": "", + "name-electron-volt": "", + "name-farad": "", + "name-femtofarad": "", + "name-gigawatt": "", + "name-henry": "", + "name-joule": "", + "name-kiloampere": "", + "name-kiloampere-hour": "", + "name-kiloohm": "", + "name-kilovolt": "", + "name-kilovolt-ampere": "", + "name-kilovolt-ampere-reactive": "", + "name-kilowatt": "", + "name-kilowatt-hour": "", + "name-kilowatt-min": "", + "name-lumens": "", + "name-megaohm": "", + "name-megawatt": "", + "name-megawatt-hour": "", + "name-microfarad": "", + "name-microhenry": "", + "name-milliampere": "", + "name-milliampere-hour": "", + "name-millihenry": "", + "name-milliohm": "", + "name-millivolt": "", + "name-milliwatt": "", + "name-nanofarad": "", + "name-ohm": "", + "name-picofarad": "", + "name-volt": "", + "name-volt-ampere": "", + "name-volt-ampere-reactive": "", + "name-watt": "", + "name-watt-hour": "", + "name-watt-hour-per-kg": "", + "name-watt-square-meter": "" + }, + "name": "" + }, + "flow": { + "formats": { + "name-cubic-feet-min": "", + "name-cubic-feet-sec": "", + "name-cubic-meters-sec": "", + "name-gallons-min": "", + "name-litre-hour": "", + "name-litre-min": "", + "name-lux": "", + "name-millilitre-min": "" + }, + "name": "" + }, + "force": { + "formats": { + "name-kilonewton-meters": "", + "name-kilonewtons": "", + "name-newton-meters": "", + "name-newtons": "" + }, + "name": "" + }, + "hash-rate": { + "formats": { + "name-exahashes-sec": "", + "name-gigahashes-sec": "", + "name-hashes-sec": "", + "name-kilohashes-sec": "", + "name-megahashes-sec": "", + "name-petahashes-sec": "", + "name-terahashes-sec": "" + }, + "name": "" + }, + "length": { + "formats": { + "name-feet": "", + "name-inch": "", + "name-kilometer": "", + "name-meter": "", + "name-mile": "", + "name-millimeter": "" + }, + "name": "" + }, + "mass": { + "formats": { + "name-gram": "", + "name-kilogram": "", + "name-metric-ton": "", + "name-milligram": "", + "name-pound": "" + }, + "name": "" + }, + "misc": { + "formats": { + "name-candala": "", + "name-decibel": "", + "name-hexadecimal": "", + "name-hexadecimal-0x": "", + "name-humidity": "", + "name-locale": "", + "name-number": "", + "name-percent-1": "", + "name-percent-100": "", + "name-pixels": "", + "name-scientific": "", + "name-short": "", + "name-si-short": "", + "name-string": "" + }, + "name": "" + }, + "pressure": { + "formats": { + "name-bars": "", + "name-hectopascals": "", + "name-inches-mercury": "", + "name-kilobars": "", + "name-kilopascals": "", + "name-millibars": "", + "name-pascals": "", + "name-psi": "" + }, + "name": "" + }, + "radiation": { + "formats": { + "name-becquerel": "", + "name-curie": "", + "name-exposure": "", + "name-gray": "", + "name-microsievert": "", + "name-microsievert-hour": "", + "name-millisievert": "", + "name-millisievert-hour": "", + "name-rad": "", + "name-rem": "", + "name-roentgen": "", + "name-sievert": "", + "name-sievert-hour": "" + }, + "name": "" + }, + "rotational-speed": { + "formats": { + "name-degrees-sec": "", + "name-gigahertz": "", + "name-hertz": "", + "name-kilohertz": "", + "name-megahertz": "", + "name-radians-sec": "", + "name-rpm": "" + }, + "name": "" + }, + "temperature": { + "formats": { + "name-celsius": "", + "name-fahrenheit": "", + "name-kelvin": "" + }, + "name": "" + }, + "throughput": { + "formats": { + "name-counts-min": "", + "name-counts-sec": "", + "name-events-min": "", + "name-events-sec": "", + "name-io-ops-sec": "", + "name-messages-min": "", + "name-messages-sec": "", + "name-ops-min": "", + "name-ops-sec": "", + "name-reads-min": "", + "name-reads-sec": "", + "name-records-min": "", + "name-records-sec": "", + "name-requests-min": "", + "name-requests-sec": "", + "name-rows-min": "", + "name-rows-sec": "", + "name-writes-min": "", + "name-writes-sec": "" + }, + "name": "" + }, + "time": { + "formats": { + "name-clock-ms": "", + "name-clock-s": "", + "name-days": "", + "name-duration-dhms": "", + "name-duration-hms": "", + "name-duration-ms": "", + "name-duration-s": "", + "name-hertz": "", + "name-hours": "", + "name-microseconds": "", + "name-milliseconds": "", + "name-minutes": "", + "name-nanoseconds": "", + "name-seconds": "", + "name-timeticks": "" + }, + "name": "" + }, + "velocity": { + "formats": { + "name-kilometers-hour": "", + "name-knot": "", + "name-meters-second": "", + "name-miles-hour": "" + }, + "name": "" + }, + "volume": { + "formats": { + "name-cubic-decimeter": "", + "name-cubic-meter": "", + "name-gallons": "", + "name-litre": "", + "name-millilitre": "", + "name-normal-cubic-meter": "" + }, + "name": "" + } + } + } + }, "grafana-ui": { "action-editor": { "button": { @@ -10544,7 +10950,9 @@ "label-display-name": "顯示名稱", "placeholder-my-repository-connection": "我的儲存庫連線", "text-loading-resource-information": "正在載入資源資訊…", - "files-count_other": "{{count}} 個檔案" + "dashboards-count_other": "", + "files-count_other": "{{count}} 個檔案", + "folders-count_other": "" }, "check-repository": { "check": "勾選" @@ -10908,6 +11316,11 @@ "label-current-step": "目前步驟", "label-pending-step": "待處理步驟" }, + "sync-job": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "" + }, "sync-repository": { "body-edit-configuration": "編輯設定", "button-edit": "編輯", @@ -10918,9 +11331,6 @@ "tooltip-unhealthy-repository": "無法拉取狀態不佳的儲存庫" }, "synchronize-step": { - "error-no-job-id": "無法啟動作業", - "error-no-repository-name": "未提供儲存庫名稱", - "error-starting-job": "開始作業時發生錯誤", "synchronization-description": "包含每個歷史數值的提交", "synchronization-options": "同步選項" }, From 08a287e7057edad8cb3d1f9e0126e58e0d16acf0 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 23 Jul 2025 10:10:33 +0300 Subject: [PATCH 002/101] Browse dashboards: Hide checkboxes for items user cannot edit (#108427) * Add test * BrowseDashboards: Separate folder and dashboard permission checks * Extract type * Check for edit permissions * Prettier * Refactor test * Update canEditItemType * Update render check * Update the check --- .../BrowseDashboardsPage.test.tsx | 107 +++++++++++++----- .../BrowseDashboardsPage.tsx | 20 +++- .../BrowseFolderAlertingPage.test.tsx | 1 + .../BrowseFolderLibraryPanelsPage.test.tsx | 1 + .../browse-dashboards/RecentlyDeletedPage.tsx | 6 +- .../components/BrowseView.test.tsx | 41 +++++-- .../components/BrowseView.tsx | 10 +- .../components/CheckboxCell.tsx | 8 +- .../components/DashboardsTree.test.tsx | 37 ++++-- .../components/DashboardsTree.tsx | 27 +++-- .../components/FolderActionsButton.test.tsx | 1 + .../components/SearchView.tsx | 23 +++- .../browse-dashboards/components/utils.ts | 17 ++- .../features/browse-dashboards/permissions.ts | 2 + .../app/features/browse-dashboards/types.ts | 8 ++ 15 files changed, 236 insertions(+), 73 deletions(-) diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index a2bfe9ca651..4a2c9f4a82b 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -1,4 +1,4 @@ -import { render as rtlRender, screen } from '@testing-library/react'; +import { render as rtlRender, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { HttpResponse, http } from 'msw'; import { setupServer, SetupServer } from 'msw/node'; @@ -120,6 +120,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canEditFolders: true, canViewPermissions: true, canSetPermissions: true, + canDeleteDashboards: true, }; beforeAll(() => { @@ -157,6 +158,17 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); afterEach(() => { + // Reset permissions back to defaults + Object.assign(mockPermissions, { + canCreateDashboards: true, + canEditDashboards: true, + canCreateFolders: true, + canDeleteFolders: true, + canEditFolders: true, + canViewPermissions: true, + canSetPermissions: true, + canDeleteDashboards: true, + }); jest.restoreAllMocks(); server.resetHandlers(); }); @@ -178,13 +190,8 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('does not show the "New" button if the user does not have permissions', async () => { - jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => { - return { - ...mockPermissions, - canCreateDashboards: false, - canCreateFolders: false, - }; - }); + mockPermissions.canCreateDashboards = false; + mockPermissions.canCreateFolders = false; render(); expect(await screen.findByRole('heading', { name: 'Dashboards' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); @@ -281,13 +288,8 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('does not show the "New" button if the user does not have permissions', async () => { - jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => { - return { - ...mockPermissions, - canCreateDashboards: false, - canCreateFolders: false, - }; - }); + mockPermissions.canCreateDashboards = false; + mockPermissions.canCreateFolders = false; render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'New' })).not.toBeInTheDocument(); @@ -299,15 +301,10 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('does not show the "Folder actions" button if the user does not have permissions', async () => { - jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => { - return { - ...mockPermissions, - canDeleteFolders: false, - canEditFolders: false, - canSetPermissions: false, - canViewPermissions: false, - }; - }); + mockPermissions.canDeleteFolders = false; + mockPermissions.canEditFolders = false; + mockPermissions.canSetPermissions = false; + mockPermissions.canViewPermissions = false; render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Folder actions' })).not.toBeInTheDocument(); @@ -319,12 +316,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { }); it('does not show the "Edit title" button if the user does not have permissions', async () => { - jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => { - return { - ...mockPermissions, - canEditFolders: false, - }; - }); + mockPermissions.canEditFolders = false; render(); expect(await screen.findByRole('heading', { name: folderA.item.title })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Edit title' })).not.toBeInTheDocument(); @@ -369,5 +361,60 @@ describe('browse-dashboards BrowseDashboardsPage', () => { expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); }); + + it('should not show checkbox for folder when user has dashboards:write but lacks folder edit permissions', async () => { + mockPermissions.canCreateFolders = false; + mockPermissions.canDeleteFolders = false; + mockPermissions.canEditFolders = false; + mockPermissions.canSetPermissions = false; + mockPermissions.canViewPermissions = false; + mockPermissions.canDeleteDashboards = false; + + render(); + + await waitFor(() => { + const checkbox = screen.queryByTestId( + selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid) + ); + + expect(checkbox).not.toBeInTheDocument(); + }); + }); + + it('should not show checkbox for folder when user has folder:write but lacks folder delete permissions', async () => { + mockPermissions.canCreateFolders = false; + mockPermissions.canDeleteFolders = false; + mockPermissions.canEditFolders = true; + mockPermissions.canSetPermissions = false; + mockPermissions.canViewPermissions = false; + + render(); + + await waitFor(() => { + const checkbox = screen.queryByTestId( + selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid) + ); + + expect(checkbox).not.toBeInTheDocument(); + }); + }); + + it('should show checkbox for folder when user has folder:write and folder delete permissions', async () => { + mockPermissions.canCreateFolders = false; + mockPermissions.canEditDashboards = false; + mockPermissions.canDeleteDashboards = false; + mockPermissions.canSetPermissions = false; + mockPermissions.canViewPermissions = false; + mockPermissions.canDeleteFolders = true; + mockPermissions.canEditFolders = true; + + render(); + + const checkbox = await screen.findByTestId( + selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid) + ); + + expect(checkbox).toBeInTheDocument(); + }); }); }); diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index f37973f7401..01e60815402 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -93,11 +93,23 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record { if (folderDTO) { const result = await saveFolder({ @@ -183,14 +195,14 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record isSearching ? ( ) : ( - + ) } diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx index bc2ffeb8caa..72c4663fa6d 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx @@ -34,6 +34,7 @@ describe('browse-dashboards BrowseFolderAlertingPage', () => { canEditFolders: true, canViewPermissions: true, canSetPermissions: true, + canDeleteDashboards: true, }; beforeEach(() => { diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx index dec629c0181..c60a8a8de56 100644 --- a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx @@ -45,6 +45,7 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { canEditFolders: true, canViewPermissions: true, canSetPermissions: true, + canDeleteDashboards: true, }; beforeAll(() => { diff --git a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx index 3e18b71d399..b564740c716 100644 --- a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx +++ b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx @@ -25,8 +25,8 @@ const RecentlyDeletedPage = memo(() => { const [searchState, stateManager] = useRecentlyDeletedStateManager(); const hasSelection = useHasSelection(); - const { canEditFolders, canEditDashboards } = getFolderPermissions(); - const canSelect = canEditFolders || canEditDashboards; + const { canEditFolders, canEditDashboards, canDeleteFolders, canDeleteDashboards } = getFolderPermissions(); + const permissions = { canEditFolders, canEditDashboards, canDeleteFolders, canDeleteDashboards }; useEffect(() => { stateManager.initStateFromUrl(undefined); @@ -75,7 +75,7 @@ const RecentlyDeletedPage = memo(() => { {({ width, height }) => ( { describe('browse-dashboards BrowseView', () => { const WIDTH = 800; const HEIGHT = 600; + const mockPermissions = { + canEditFolders: true, + canEditDashboards: true, + canDeleteFolders: true, + canDeleteDashboards: true, + }; + + afterEach(() => { + // Reset permissions back to defaults + Object.assign(mockPermissions, { + canEditFolders: true, + canEditDashboards: true, + canDeleteFolders: true, + canDeleteDashboards: true, + }); + }); it('expands and collapses a folder', async () => { - render(); + render(); await screen.findByText(folderA.item.title); await expandFolder(folderA.item); @@ -55,7 +71,7 @@ describe('browse-dashboards BrowseView', () => { }); it('checks items when selected', async () => { - render(); + render(); const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid)); expect(checkbox).not.toBeChecked(); @@ -65,7 +81,7 @@ describe('browse-dashboards BrowseView', () => { }); it('checks all descendants when a folder is selected', async () => { - render(); + render(); await screen.findByText(folderA.item.title); // First expand then click folderA @@ -82,7 +98,7 @@ describe('browse-dashboards BrowseView', () => { }); it('checks descendants loaded after a folder is selected', async () => { - render(); + render(); await screen.findByText(folderA.item.title); // First expand then click folderA @@ -102,7 +118,7 @@ describe('browse-dashboards BrowseView', () => { }); it('unchecks ancestors when unselecting an item', async () => { - render(); + render(); await screen.findByText(folderA.item.title); await expandFolder(folderA.item); @@ -126,7 +142,7 @@ describe('browse-dashboards BrowseView', () => { }); it('shows indeterminate checkboxes when a descendant is selected', async () => { - render(); + render(); await screen.findByText(folderA.item.title); await expandFolder(folderA.item); @@ -147,12 +163,21 @@ describe('browse-dashboards BrowseView', () => { describe('when there is no item in the folder', () => { it('shows a CTA for creating a dashboard if the user has editor rights', async () => { - render(); + render( + + ); expect(await screen.findByText('Create dashboard')).toBeInTheDocument(); }); it('shows a simple message if the user has viewer rights', async () => { - render(); + mockPermissions.canEditFolders = false; + mockPermissions.canEditDashboards = false; + mockPermissions.canDeleteFolders = false; + mockPermissions.canDeleteDashboards = false; + + render( + + ); expect(await screen.findByText('This folder is empty')).toBeInTheDocument(); }); }); diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx index b20fa522fc7..5cd79f3341f 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -15,23 +15,25 @@ import { useLoadNextChildrenPage, } from '../state/hooks'; import { setFolderOpenState, setItemSelectionState, setAllSelection } from '../state/slice'; -import { BrowseDashboardsState, DashboardTreeSelection, SelectionState } from '../types'; +import { BrowseDashboardsState, DashboardTreeSelection, SelectionState, BrowseDashboardsPermissions } from '../types'; import { DashboardsTree } from './DashboardsTree'; +import { canSelectItems } from './utils'; interface BrowseViewProps { height: number; width: number; folderUID: string | undefined; - canSelect: boolean; + permissions: BrowseDashboardsPermissions; } -export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewProps) { +export function BrowseView({ folderUID, width, height, permissions }: BrowseViewProps) { const status = useBrowseLoadingStatus(folderUID); const dispatch = useDispatch(); const flatTree = useFlatTreeState(folderUID); const selectedItems = useCheckboxSelectionState(); const childrenByParentUID = useChildrenByParentUIDState(); + const canSelect = canSelectItems(permissions); const handleFolderClick = useCallback( (clickedFolderUID: string, isOpen: boolean) => { @@ -156,7 +158,7 @@ export function BrowseView({ folderUID, width, height, canSelect }: BrowseViewPr return ( ; } + // Check if user can edit this specific item type + if (permissions && !canEditItemType(item.kind, permissions)) { + return ; + } + const state = isSelected(item); return ( diff --git a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx index bc2bebe5cce..e8109397870 100644 --- a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx +++ b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx @@ -23,6 +23,12 @@ function render(...[ui, options]: Parameters) { describe('browse-dashboards DashboardsTree', () => { const WIDTH = 800; const HEIGHT = 600; + const mockPermissions = { + canEditFolders: true, + canEditDashboards: true, + canDeleteFolders: true, + canDeleteDashboards: true, + }; const folder = wellFormedFolder(1); const emptyFolderIndicator = wellFormedEmptyFolder(); @@ -36,10 +42,20 @@ describe('browse-dashboards DashboardsTree', () => { config.sharedWithMeFolderUID = 'sharedwithme'; }); + afterEach(() => { + // Reset permissions back to defaults + Object.assign(mockPermissions, { + canEditFolders: true, + canEditDashboards: true, + canDeleteFolders: true, + canDeleteDashboards: true, + }); + }); + it('renders a dashboard item', () => { render( { }); it('does not render checkbox when disabled', () => { + mockPermissions.canEditFolders = false; + mockPermissions.canEditDashboards = false; + mockPermissions.canDeleteFolders = false; + mockPermissions.canDeleteDashboards = false; + render( { it('renders a folder item', () => { render( { it('renders a folder link', () => { render( { render( { render( { const handler = jest.fn(); render( { it('renders empty folder indicators', () => { render( SelectionState; onFolderClick: (uid: string, newOpenState: boolean) => void; onAllSelectionChange: (newState: boolean) => void; @@ -48,7 +54,7 @@ export function DashboardsTree({ onItemSelectionChange, isItemLoaded, requestLoadMore, - canSelect = false, + permissions, }: DashboardsTreeProps) { const treeID = useId(); @@ -94,10 +100,11 @@ export function DashboardsTree({ Header: t('browse-dashboards.dashboards-tree.tags-column', 'Tags'), Cell: TagsCell, }; + const canSelect = canSelectItems(permissions); const columns = [canSelect && checkboxColumn, nameColumn, tagsColumns].filter(isTruthy); return columns; - }, [onFolderClick, canSelect]); + }, [onFolderClick, permissions]); const table = useTable({ columns: tableColumns, data: items }, useCustomFlexLayout); const { getTableProps, getTableBodyProps, headerGroups } = table; @@ -109,10 +116,11 @@ export function DashboardsTree({ onAllSelectionChange, onItemSelectionChange, treeID, + permissions, }), // we need this to rerender if items changes // eslint-disable-next-line react-hooks/exhaustive-deps - [table, isSelected, onAllSelectionChange, onItemSelectionChange, items, treeID] + [table, isSelected, onAllSelectionChange, onItemSelectionChange, items, treeID, permissions] ); const handleIsItemLoaded = useCallback( @@ -156,7 +164,7 @@ export function DashboardsTree({ return (
- {column.render('Header', { isSelected, onAllSelectionChange })} + {column.render('Header', { isSelected, onAllSelectionChange, permissions })}
); })} @@ -203,12 +211,13 @@ interface VirtualListRowProps { onAllSelectionChange: DashboardsTreeCellProps['onAllSelectionChange']; onItemSelectionChange: DashboardsTreeCellProps['onItemSelectionChange']; treeID: string; + permissions: BrowseDashboardsPermissions; }; } function VirtualListRow({ index, style, data }: VirtualListRowProps) { const styles = useStyles2(getStyles); - const { table, isSelected, onItemSelectionChange, treeID } = data; + const { table, isSelected, onItemSelectionChange, treeID, permissions } = data; const { rows, prepareRow } = table; const row = rows[index]; @@ -240,7 +249,7 @@ function VirtualListRow({ index, style, data }: VirtualListRowProps) { return (
- {cell.render('Cell', { isSelected, onItemSelectionChange, treeID })} + {cell.render('Cell', { isSelected, onItemSelectionChange, treeID, permissions })}
); })} diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx index 535be61f806..dd019520aeb 100644 --- a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx +++ b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx @@ -32,6 +32,7 @@ describe('browse-dashboards FolderActionsButton', () => { canEditFolders: true, canViewPermissions: true, canSetPermissions: true, + canDeleteDashboards: true, }; beforeEach(() => { diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx index 281455f140e..7f7b1c9cbb3 100644 --- a/public/app/features/browse-dashboards/components/SearchView.tsx +++ b/public/app/features/browse-dashboards/components/SearchView.tsx @@ -11,11 +11,14 @@ import { useDispatch, useSelector } from 'app/types/store'; import { useHasSelection } from '../state/hooks'; import { setAllSelection, setItemSelectionState } from '../state/slice'; +import { BrowseDashboardsPermissions } from '../types'; + +import { canEditItemType, canSelectItems } from './utils'; interface SearchViewProps { height: number; width: number; - canSelect: boolean; + permissions: BrowseDashboardsPermissions; searchState: SearchState; searchStateManager: SearchStateManager; emptyState?: ReactNode; @@ -48,7 +51,7 @@ const initialLoadingView = { export function SearchView({ width, height, - canSelect, + permissions, searchState, searchStateManager: stateManager, emptyState: emptyStateProp, @@ -67,7 +70,12 @@ export function SearchView({ return false; } - // Currently, this indicates _some_ items are selected, not nessicarily all are + // Check if user has permission to select this item type + if (!canEditItemType(kind, permissions)) { + return false; + } + + // Currently, this indicates _some_ items are selected, not necessarily all are // selected. if (kind === '*' && uid === '*') { return hasSelection; @@ -78,7 +86,7 @@ export function SearchView({ return selectedItems[assertDashboardViewItemKind(kind)][uid] ?? false; }, - [selectedItems, hasSelection] + [selectedItems, hasSelection, permissions] ); const clearSelection = useCallback(() => { @@ -87,13 +95,17 @@ export function SearchView({ const handleItemSelectionChange = useCallback( (kind: string, uid: string) => { + if (!canEditItemType(kind, permissions)) { + return; // Cannot select this item + } + const newIsSelected = !selectionChecker(kind, uid); dispatch( setItemSelectionState({ item: { kind: assertDashboardViewItemKind(kind), uid }, isSelected: newIsSelected }) ); }, - [selectionChecker, dispatch] + [selectionChecker, dispatch, permissions] ); if (value.totalRows === 0) { @@ -113,6 +125,7 @@ export function SearchView({ return
{emptyState}
; } + const canSelect = canSelectItems(permissions); const props: SearchResultsProps = { response: value, selection: canSelect ? selectionChecker : undefined, diff --git a/public/app/features/browse-dashboards/components/utils.ts b/public/app/features/browse-dashboards/components/utils.ts index 0427ba787a0..66a85a6d0ee 100644 --- a/public/app/features/browse-dashboards/components/utils.ts +++ b/public/app/features/browse-dashboards/components/utils.ts @@ -6,7 +6,7 @@ import { DashboardViewItem } from 'app/features/search/types'; import { useChildrenByParentUIDState } from '../state/hooks'; import { findItem } from '../state/utils'; -import { DashboardTreeSelection, DashboardViewItemWithUIItems } from '../types'; +import { DashboardTreeSelection, DashboardViewItemWithUIItems, BrowseDashboardsPermissions } from '../types'; export function makeRowID(baseId: string, item: DashboardViewItemWithUIItems) { return baseId + item.uid; @@ -105,3 +105,18 @@ export function collectSelectedItems( return targets; } + +export function canEditItemType(itemKind: string, permissions: BrowseDashboardsPermissions) { + const { canEditFolders, canDeleteFolders, canEditDashboards, canDeleteDashboards } = permissions; + return itemKind === 'folder' + ? Boolean(canEditFolders || canDeleteFolders) + : Boolean(canEditDashboards || canDeleteDashboards); +} + +export function canSelectItems(permissions: BrowseDashboardsPermissions) { + const { canEditFolders, canDeleteFolders, canEditDashboards, canDeleteDashboards } = permissions; + // Users can select items only if they have both edit and delete permissions for at least one item type + const canSelectFolders = canEditFolders || canDeleteFolders; + const canSelectDashboards = canEditDashboards || canDeleteDashboards; + return Boolean(canSelectFolders || canSelectDashboards); +} diff --git a/public/app/features/browse-dashboards/permissions.ts b/public/app/features/browse-dashboards/permissions.ts index d589af9480a..ff7839bf2de 100644 --- a/public/app/features/browse-dashboards/permissions.ts +++ b/public/app/features/browse-dashboards/permissions.ts @@ -20,6 +20,7 @@ export function getFolderPermissions(folderDTO?: FolderDTO) { const canCreateDashboards = checkFolderPermission(AccessControlAction.DashboardsCreate, folderDTO); const canCreateFolders = checkCanCreateFolders(folderDTO); const canDeleteFolders = checkFolderPermission(AccessControlAction.FoldersDelete, folderDTO); + const canDeleteDashboards = checkFolderPermission(AccessControlAction.DashboardsDelete, folderDTO); const canEditDashboards = checkFolderPermission(AccessControlAction.DashboardsWrite, folderDTO); const canEditFolders = checkFolderPermission(AccessControlAction.FoldersWrite, folderDTO); const canSetPermissions = checkFolderPermission(AccessControlAction.FoldersPermissionsWrite, folderDTO); @@ -33,5 +34,6 @@ export function getFolderPermissions(folderDTO?: FolderDTO) { canEditFolders, canSetPermissions, canViewPermissions, + canDeleteDashboards, }; } diff --git a/public/app/features/browse-dashboards/types.ts b/public/app/features/browse-dashboards/types.ts index db797d0b0f6..795d3bb794d 100644 --- a/public/app/features/browse-dashboards/types.ts +++ b/public/app/features/browse-dashboards/types.ts @@ -49,6 +49,7 @@ interface RendererUserProps { onAllSelectionChange?: (newState: boolean) => void; onItemSelectionChange?: (item: DashboardViewItem, newState: boolean) => void; treeID?: string; + permissions?: BrowseDashboardsPermissions; } export type DashboardsTreeColumn = Column; @@ -60,3 +61,10 @@ export enum SelectionState { Selected, Mixed, } + +export interface BrowseDashboardsPermissions { + canEditFolders: boolean; + canEditDashboards: boolean; + canDeleteFolders?: boolean; + canDeleteDashboards?: boolean; +} From aedd7b6e3408a04a84ef02c535439fd17aa2e0e6 Mon Sep 17 00:00:00 2001 From: Sam Jewell <2903904+samjewell@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:12:25 +0100 Subject: [PATCH 003/101] [--Dashboard-- data source] Implement AdHoc filtering (#108011) Behind a feature toggle: `dashboardDsAdHocFiltering` --- .../src/types/featureToggles.gen.ts | 4 + pkg/services/featuremgmt/codeowners.go | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 13 + .../components/AdHocVariableForm.tsx | 10 +- .../datasource/dashboard/datasource.test.ts | 442 +++++++++++++++++- .../datasource/dashboard/datasource.ts | 171 ++++++- 9 files changed, 647 insertions(+), 6 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index db6b0327350..534d8064bbc 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1054,4 +1054,8 @@ export interface FeatureToggles { * Enable dual reader for unified storage search */ unifiedStorageSearchDualReaderEnabled?: boolean; + /** + * Enables adhoc filtering support for the dashboard datasource + */ + dashboardDsAdHocFiltering?: boolean; } diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index e4647a59a6f..75e868fd57b 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -8,6 +8,7 @@ const ( grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" + grafanaDataProSquad codeowner = "@grafana/datapro" grafanaFrontendPlatformSquad codeowner = "@grafana/grafana-frontend-platform" grafanaFrontendSearchNavOrganise codeowner = "@grafana/grafana-search-navigate-organise" grafanaBackendServicesSquad codeowner = "@grafana/grafana-backend-services-squad" diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index b595a5bf480..35b8b06872d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1821,6 +1821,13 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, + { + Name: "dashboardDsAdHocFiltering", + Description: "Enables adhoc filtering support for the dashboard datasource", + Stage: FeatureStageExperimental, + Owner: grafanaDataProSquad, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b3731aa5c3f..7c8dbf691f4 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -235,3 +235,4 @@ otelLogsFormatting,experimental,@grafana/observability-logs,false,false,true alertingNotificationHistory,experimental,@grafana/alerting-squad,false,false,false pluginAssetProvider,experimental,@grafana/plugins-platform-backend,false,true,false unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,false,false,false +dashboardDsAdHocFiltering,experimental,@grafana/datapro,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index a6a36e494e9..c94945edc9a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -950,4 +950,8 @@ const ( // FlagUnifiedStorageSearchDualReaderEnabled // Enable dual reader for unified storage search FlagUnifiedStorageSearchDualReaderEnabled = "unifiedStorageSearchDualReaderEnabled" + + // FlagDashboardDsAdHocFiltering + // Enables adhoc filtering support for the dashboard datasource + FlagDashboardDsAdHocFiltering = "dashboardDsAdHocFiltering" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 07d86946b21..f66d89f5c28 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -796,6 +796,19 @@ "codeowner": "@grafana/grafana-app-platform-squad" } }, + { + "metadata": { + "name": "dashboardDsAdHocFiltering", + "resourceVersion": "1752487974435", + "creationTimestamp": "2025-07-14T10:12:54Z" + }, + "spec": { + "description": "Enables adhoc filtering support for the dashboard datasource", + "stage": "experimental", + "codeowner": "@grafana/datapro", + "frontend": true + } + }, { "metadata": { "name": "dashboardNewLayouts", diff --git a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx index fcdf7881fce..29183c79531 100644 --- a/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/components/AdHocVariableForm.tsx @@ -4,6 +4,7 @@ import { DataSourceInstanceSettings, MetricFindValue, readCSV } from '@grafana/d import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { EditorField } from '@grafana/plugin-ui'; +import { config } from '@grafana/runtime'; import { DataSourceRef } from '@grafana/schema'; import { Alert, CodeEditor, Field, Switch, Box } from '@grafana/ui'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; @@ -61,7 +62,14 @@ export function AdHocVariableForm({ htmlFor="data-source-picker" tooltip={infoText} > - + diff --git a/public/app/plugins/datasource/dashboard/datasource.test.ts b/public/app/plugins/datasource/dashboard/datasource.test.ts index 26413c094df..3bb2fbc6b3c 100644 --- a/public/app/plugins/datasource/dashboard/datasource.test.ts +++ b/public/app/plugins/datasource/dashboard/datasource.test.ts @@ -7,15 +7,19 @@ import { getDefaultTimeRange, LoadingState, standardTransformersRegistry, + FieldType, + DataFrame, + AdHocVariableFilter, } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test'; -import { setPluginImportUtils } from '@grafana/runtime'; +import { setPluginImportUtils, config } from '@grafana/runtime'; import { SafeSerializableSceneObject, SceneDataNode, SceneDataTransformer, SceneFlexItem, SceneFlexLayout, + SceneObject, VizPanel, } from '@grafana/scenes'; import { getVizPanelKeyForPanelId } from 'app/features/dashboard-scene/utils/utils'; @@ -131,6 +135,441 @@ describe('DashboardDatasource', () => { // on further emissions the result should be the unmodified original dataframe expect(rsp!.data[0].fields[0].state).toEqual({}); }); + + describe('AdHoc Filtering', () => { + // Shared test utilities + interface TestField { + name: string; + type: FieldType; + values: unknown[]; + } + + function createTestFrame(fields: TestField[]) { + return { + name: 'TestData', + fields: fields.map((field) => ({ + name: field.name, + type: field.type, + values: field.values, + config: {}, + state: {}, + })), + length: fields[0]?.values.length || 0, + refId: 'A', + }; + } + + function createQueryRequest(filters: AdHocVariableFilter[], scene: SceneObject) { + return { + timezone: 'utc', + targets: [{ refId: 'A', panelId: 1 }], + requestId: '', + interval: '', + intervalMs: 0, + range: getDefaultTimeRange(), + scopedVars: { + __sceneObject: new SafeSerializableSceneObject(scene), + }, + app: '', + startTime: 0, + filters: filters, + }; + } + + // Test AdHoc filtering via the Public API first, to ensure Integration + describe('Integration (Public API)', () => { + const originalToggleValue = config.featureToggles.dashboardDsAdHocFiltering; + + beforeEach(() => { + config.featureToggles.dashboardDsAdHocFiltering = true; + }); + + afterEach(() => { + config.featureToggles.dashboardDsAdHocFiltering = originalToggleValue; + }); + + it('should apply basic filtering end-to-end through public query method', async () => { + const testFrame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const scene = new SceneFlexLayout({ + children: [ + new SceneFlexItem({ + body: new VizPanel({ + key: getVizPanelKeyForPanelId(1), + $data: new SceneDataNode({ + data: { + series: [testFrame], + state: LoadingState.Done, + timeRange: getDefaultTimeRange(), + }, + }), + }), + }), + ], + }); + + const ds = new DashboardDatasource({} as DataSourceInstanceSettings); + const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }]; + + const observable = ds.query(createQueryRequest(filters, scene)); + + let result: DataQueryResponse | undefined; + observable.subscribe({ next: (data) => (result = data) }); + + expect(result?.data[0].fields[0].values).toEqual(['John']); + expect(result?.data[0].fields[1].values).toEqual([25]); + expect(result?.data[0].length).toBe(1); + }); + + it('should respect feature toggle and not filter when disabled', async () => { + // Temporarily disable the feature toggle for this test + config.featureToggles.dashboardDsAdHocFiltering = false; + + const testFrame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const scene = new SceneFlexLayout({ + children: [ + new SceneFlexItem({ + body: new VizPanel({ + key: getVizPanelKeyForPanelId(1), + $data: new SceneDataNode({ + data: { + series: [testFrame], + state: LoadingState.Done, + timeRange: getDefaultTimeRange(), + }, + }), + }), + }), + ], + }); + + const ds = new DashboardDatasource({} as DataSourceInstanceSettings); + const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }]; + + const observable = ds.query(createQueryRequest(filters, scene)); + + let result: DataQueryResponse | undefined; + observable.subscribe({ next: (data) => (result = data) }); + + // Should return unfiltered data since feature toggle is disabled + expect(result?.data[0].fields[0].values).toEqual(['John', 'Jane', 'Bob']); + expect(result?.data[0].fields[1].values).toEqual([25, 30, 35]); + expect(result?.data[0].length).toBe(3); + }); + + it('should apply multiple filters with AND logic through public API', async () => { + const testFrame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'status', type: FieldType.string, values: ['active', 'active', 'inactive'] }, + ]); + + const scene = new SceneFlexLayout({ + children: [ + new SceneFlexItem({ + body: new VizPanel({ + key: getVizPanelKeyForPanelId(1), + $data: new SceneDataNode({ + data: { + series: [testFrame], + state: LoadingState.Done, + timeRange: getDefaultTimeRange(), + }, + }), + }), + }), + ], + }); + + const ds = new DashboardDatasource({} as DataSourceInstanceSettings); + const filters: AdHocVariableFilter[] = [ + { key: 'name', operator: '!=', value: 'John' }, + { key: 'status', operator: '=', value: 'active' }, + ]; + + const observable = ds.query(createQueryRequest(filters, scene)); + + let result: DataQueryResponse | undefined; + observable.subscribe({ next: (data) => (result = data) }); + + expect(result?.data[0].fields[0].values).toEqual(['Jane']); + expect(result?.data[0].fields[1].values).toEqual(['active']); + expect(result?.data[0].length).toBe(1); + }); + }); + + // Now test AdHoc filtering with unit-tests to test all aspects of behaviour. + // Here we test a private method, which allows for smaller, simpler, + // faster tests. + // This is the bottom of the 'Test Pyramid' + describe('Algorithm (but by testing a Private Method)', () => { + const ds = new DashboardDatasource({} as DataSourceInstanceSettings); + + // Type-safe interface for accessing private methods in tests + interface DashboardDatasourceWithPrivateMethods { + applyAdHocFilters: (frame: DataFrame, filters: AdHocVariableFilter[]) => DataFrame; + } + + it('should apply equality filter correctly', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'name', operator: '=', value: 'John' }, + ]); + + expect(result.fields[0].values).toEqual(['John']); + expect(result.fields[1].values).toEqual([25]); + expect(result.length).toBe(1); + }); + + it('should apply not-equal filter correctly', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'name', operator: '!=', value: 'John' }, + ]); + + expect(result.fields[0].values).toEqual(['Jane', 'Bob']); + expect(result.fields[1].values).toEqual([30, 35]); + expect(result.length).toBe(2); + }); + + it('should apply multiple filters with AND logic', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'status', type: FieldType.string, values: ['active', 'active', 'inactive'] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'name', operator: '!=', value: 'John' }, + { key: 'status', operator: '=', value: 'active' }, + ]); + + expect(result.fields[0].values).toEqual(['Jane']); + expect(result.fields[1].values).toEqual(['active']); + expect(result.length).toBe(1); + }); + + it('should handle null values correctly', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', null, 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'name', operator: '!=', value: 'John' }, + ]); + + expect(result.fields[0].values).toEqual([null, 'Bob']); + expect(result.fields[1].values).toEqual([30, 35]); + expect(result.length).toBe(2); + }); + + it('should apply equality filter on numeric fields', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'age', operator: '=', value: '30' }, + ]); + + expect(result.fields[0].values).toEqual(['Jane']); + expect(result.fields[1].values).toEqual([30]); + expect(result.length).toBe(1); + }); + + it('should apply not-equal filter on numeric fields', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'age', operator: '!=', value: '30' }, + ]); + + expect(result.fields[0].values).toEqual(['John', 'Bob']); + expect(result.fields[1].values).toEqual([25, 35]); + expect(result.length).toBe(2); + }); + + it('should handle numeric fields with null values', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, null, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'age', operator: '!=', value: '25' }, + ]); + + expect(result.fields[0].values).toEqual(['Jane', 'Bob']); + expect(result.fields[1].values).toEqual([null, 35]); + expect(result.length).toBe(2); + }); + + it('should handle mixed string and numeric filtering', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob', 'Alice'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 25, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'name', operator: '!=', value: 'Bob' }, + { key: 'age', operator: '=', value: '25' }, + ]); + + // Should match: name != 'Bob' AND age = 25 + // John: !Bob + 25 ✓ + // Jane: !Bob + 30 ✗ + // Bob: Bob + 25 ✗ + // Alice: !Bob + 35 ✗ + expect(result.fields[0].values).toEqual(['John']); + expect(result.fields[1].values).toEqual([25]); + expect(result.length).toBe(1); + }); + + it('should handle floating point numbers correctly', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['Alice', 'Bob', 'Charlie', 'Diana'] }, + { name: 'score', type: FieldType.number, values: [95.5, 87.25, 95.5, 92.0] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'score', operator: '=', value: '95.5' }, + ]); + + expect(result.fields[0].values).toEqual(['Alice', 'Charlie']); + expect(result.fields[1].values).toEqual([95.5, 95.5]); + expect(result.length).toBe(2); + }); + + it('should handle floating point numbers with != operator', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['Alice', 'Bob', 'Charlie'] }, + { name: 'temperature', type: FieldType.number, values: [98.6, 99.1, 98.6] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'temperature', operator: '!=', value: '98.6' }, + ]); + + expect(result.fields[0].values).toEqual(['Bob']); + expect(result.fields[1].values).toEqual([99.1]); + expect(result.length).toBe(1); + }); + + it('should handle precision edge cases with floats', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['Test1', 'Test2', 'Test3'] }, + { name: 'value', type: FieldType.number, values: [0.1 + 0.2, 0.3, 1.0000001] }, + ]); + + // Note: 0.1 + 0.2 !== 0.3 in JavaScript due to floating point precision + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'value', operator: '=', value: '0.3' }, + ]); + + // Should only match the exact 0.3, not the computed 0.1 + 0.2 + expect(result.fields[0].values).toEqual(['Test2']); + expect(result.fields[1].values).toEqual([0.3]); + expect(result.length).toBe(1); + }); + + it('should handle empty data frames', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: [] }, + { name: 'age', type: FieldType.number, values: [] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'name', operator: '=', value: 'John' }, + ]); + + expect(result.fields[0].values).toEqual([]); + expect(result.fields[1].values).toEqual([]); + expect(result.length).toBe(0); + }); + + it.skip('should handle remaining operators', () => { + // Not yet implemented, so we explicitly don't specify any behaviour for this + }); + + it.skip('should handle remaining field types (eg. date)', () => { + // Not yet implemented, so we explicitly don't specify any behaviour for this + }); + + it('should handle filters on missing fields with = operator', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'missing_field', operator: '=', value: 'test' }, + ]); + + // Should return empty result since field doesn't exist + expect(result.fields[0].values).toEqual([]); + expect(result.fields[1].values).toEqual([]); + expect(result.length).toBe(0); + }); + + it('should handle filters on missing fields with != operator', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'missing_field', operator: '!=', value: 'test' }, + ]); + + // Should return all rows since field doesn't exist (all rows are "not equal") + expect(result.fields[0].values).toEqual(['John', 'Jane', 'Bob']); + expect(result.fields[1].values).toEqual([25, 30, 35]); + expect(result.length).toBe(3); + }); + + it('should handle complex filtering scenario', () => { + const frame = createTestFrame([ + { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Admin', 'Bob'] }, + { name: 'status', type: FieldType.string, values: ['active', 'inactive', 'active', 'active'] }, + { name: 'age', type: FieldType.number, values: [25, 30, 35, 40] }, + ]); + + const result = (ds as unknown as DashboardDatasourceWithPrivateMethods).applyAdHocFilters(frame, [ + { key: 'status', operator: '=', value: 'active' }, + { key: 'name', operator: '!=', value: 'Admin' }, + { key: 'missing_field', operator: '!=', value: 'ignored' }, // Should be ignored + ]); + + // Should match: status=active AND name!=Admin + // John: active + !Admin ✓ + // Jane: inactive + !Admin ✗ + // Admin: active + Admin ✗ + // Bob: active + !Admin ✓ + expect(result.fields[0].values).toEqual(['John', 'Bob']); + expect(result.fields[1].values).toEqual(['active', 'active']); + expect(result.fields[2].values).toEqual([25, 40]); + expect(result.length).toBe(2); + }); + }); + }); }); function setup(query: DashboardQuery, requestId?: string) { @@ -140,7 +579,6 @@ function setup(query: DashboardQuery, requestId?: string) { series: [arrayToDataFrame([1, 2, 3])], state: LoadingState.Done, timeRange: getDefaultTimeRange(), - structureRev: 11, }, }), transformations: [{ id: 'reduce', options: {} }], diff --git a/public/app/plugins/datasource/dashboard/datasource.ts b/public/app/plugins/datasource/dashboard/datasource.ts index a9e34586d4f..e98e140eb0a 100644 --- a/public/app/plugins/datasource/dashboard/datasource.ts +++ b/public/app/plugins/datasource/dashboard/datasource.ts @@ -12,7 +12,13 @@ import { DataFrame, LoadingState, Field, + FieldType, + AdHocVariableFilter, + MetricFindValue, + getValueMatcher, + ValueMatcherID, } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { SceneDataProvider, SceneDataTransformer, SceneObject } from '@grafana/scenes'; import { activateSceneObjectAndParentTree, @@ -71,6 +77,9 @@ export class DashboardDatasource extends DataSourceApi { return of({ data: [] }); } + // Extract AdHoc filters from the request + const adHocFilters = options.filters || []; + return defer(() => { if (!sourceDataProvider!.isActive && sourceDataProvider?.setContainerWidth) { sourceDataProvider?.setContainerWidth(500); @@ -82,7 +91,7 @@ export class DashboardDatasource extends DataSourceApi { debounceTime(50), map((result) => { return { - data: this.getDataFramesForQueryTopic(result.data, query), + data: this.getDataFramesForQueryTopic(result.data, query, adHocFilters), state: result.data.state, errors: result.data.errors, error: result.data.error, @@ -95,7 +104,11 @@ export class DashboardDatasource extends DataSourceApi { }); } - private getDataFramesForQueryTopic(data: PanelData, query: DashboardQuery): DataFrame[] { + private getDataFramesForQueryTopic( + data: PanelData, + query: DashboardQuery, + filters: AdHocVariableFilter[] + ): DataFrame[] { const annotations = data.annotations ?? []; if (query.topic === DataTopic.Annotations) { return annotations.map((frame) => ({ @@ -111,6 +124,13 @@ export class DashboardDatasource extends DataSourceApi { ...s, fields: s.fields.map((field: Field) => ({ ...field, + config: { + ...field.config, + // Enable AdHoc filtering for string and numeric fields only when feature toggle is enabled + filterable: config.featureToggles.dashboardDsAdHocFiltering + ? field.type === FieldType.string || field.type === FieldType.number + : field.config.filterable, + }, state: { ...field.state, }, @@ -118,10 +138,149 @@ export class DashboardDatasource extends DataSourceApi { }; }); - return [...series, ...annotations]; + if (!config.featureToggles.dashboardDsAdHocFiltering || filters.length === 0) { + return [...series, ...annotations]; + } + + // Apply AdHoc filters to series data + const filteredSeries = series.map((frame) => this.applyAdHocFilters(frame, filters)); + return [...filteredSeries, ...annotations]; } } + /** + * Apply AdHoc filters to a DataFrame + * Optimized version with pre-computed field indices and value matchers for better performance + */ + private applyAdHocFilters(frame: DataFrame, filters: AdHocVariableFilter[]): DataFrame { + if (filters.length === 0 || frame.length === 0) { + return frame; + } + + // Pre-compute field indices and value matchers for better performance + const filterFieldIndices = filters + .map((filter) => { + const fieldIndex = frame.fields.findIndex((f) => f.name === filter.key); + return { filter, fieldIndex, matcher: this.createValueMatcher(filter, fieldIndex, frame) }; + }) + .filter(({ filter, fieldIndex, matcher }) => { + // If field is not present: + // - Keep filters with '=' operator (will always be false - reject rows) + // - Remove filters with '!=' operator (will always be true - no effect) + if (fieldIndex === -1) { + return filter.operator === '='; + } + // Only keep filters with valid matchers + return matcher !== null; + }); + + // If no filters remain after optimization, return original frame + if (filterFieldIndices.length === 0) { + return frame; + } + + // Short-circuit: if any filter has '=' operator with missing field, reject all rows + const hasImpossibleFilter = filterFieldIndices.some(({ fieldIndex }) => fieldIndex === -1); + if (hasImpossibleFilter) { + return this.reconstructDataFrame(frame); + } + + const matchingRows = new Set(); + + // Check each row to see if it matches all filters (AND logic) + for (let rowIndex = 0; rowIndex < frame.length; rowIndex++) { + const rowMatches = filterFieldIndices.every(({ matcher, fieldIndex }) => { + const field = frame.fields[fieldIndex]; + + // Use Grafana's value matcher system + return matcher?.(rowIndex, field, frame, [frame]) ?? false; + }); + + if (rowMatches) { + matchingRows.add(rowIndex); + } + } + + // Early return if no filtering occurred + if (matchingRows.size === frame.length) { + return frame; + } + + return this.reconstructDataFrame(frame, matchingRows); + } + + /** + * Create a value matcher from an AdHoc filter + */ + private createValueMatcher(filter: AdHocVariableFilter, fieldIndex: number, frame: DataFrame) { + // Return null for missing fields - they are handled separately + if (fieldIndex === -1) { + return null; + } + + const field = frame.fields[fieldIndex]; + + // Only support string and numeric fields when feature toggle is enabled + if (config.featureToggles.dashboardDsAdHocFiltering) { + if (field.type !== FieldType.string && field.type !== FieldType.number) { + return null; + } + } + + // Map operator to matcher ID + let matcherId: ValueMatcherID; + switch (filter.operator) { + case '=': + matcherId = ValueMatcherID.equal; + break; + case '!=': + matcherId = ValueMatcherID.notEqual; + break; + default: + return null; // Unknown operator + } + + try { + return getValueMatcher({ + id: matcherId, + options: { value: filter.value }, + }); + } catch (error) { + console.warn('Failed to create value matcher for filter:', filter, error); + return null; + } + } + + /** + * Reconstruct DataFrame with only matching rows + * Optimized to avoid repeated array operations + */ + private reconstructDataFrame(frame: DataFrame, matchingRows?: Set): DataFrame { + // Default to empty set if no matching rows provided (reject all rows) + const rows = matchingRows ?? new Set(); + + const fields: Field[] = frame.fields.map((field) => { + // Pre-allocate array and use direct assignment for better performance with large datasets + const newValues = new Array(rows.size); + let i = 0; + for (const rowIndex of rows) { + newValues[i++] = field.values[rowIndex]; + } + + return { + ...field, + values: newValues, + state: {}, // Clean the state as it's being recalculated + }; + }); + + return { + ...frame, + fields: fields, + length: rows.size, + }; + } + private findSourcePanel(scene: SceneObject, panelId: number) { // We're trying to find the original panel, not a cloned one, since `panelId` alone cannot resolve clones return findOriginalVizPanelByKey(scene, getVizPanelKeyForPanelId(panelId)); @@ -169,4 +328,10 @@ export class DashboardDatasource extends DataSourceApi { testDatasource(): Promise { return Promise.resolve({ message: '', status: '' }); } + + getTagKeys(): Promise { + // Stub implementation to indicate AdHoc filter support + // Full implementation will be added in future PRs + return Promise.resolve([]); + } } From b2688dd5ffd406508415561d2dbc933f2eff26dd Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 23 Jul 2025 09:23:05 +0100 Subject: [PATCH 004/101] I18n: Default regional format to Accept-Language header value (#108468) * I18n: Default regional format to Accept-Language header value * comment * rejigger * rejigger preferences --- pkg/api/index.go | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index 19db36d3217..cda365a9537 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -69,8 +69,18 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV // Locale is used for some number and date/time formatting, whereas language is used just for // translating words in the interface acceptLangHeader := c.Req.Header.Get("Accept-Language") + acceptLangHeaderFirstValue := "" + + if len(acceptLangHeader) > 0 { + parts := strings.Split(acceptLangHeader, ",") + acceptLangHeaderFirstValue = parts[0] + } + locale := "en-US" // default to en formatting, but use the accept-lang header or user's preference - var regionalFormat string + if acceptLangHeaderFirstValue != "" { + locale = acceptLangHeaderFirstValue + } + language := "" // frontend will set the default language urlPrefs := getURLPrefs(c) @@ -80,17 +90,27 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV language = prefs.JSONData.Language } - if len(acceptLangHeader) > 0 { - parts := strings.Split(acceptLangHeader, ",") - locale = parts[0] - } - + var regionalFormat string if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagLocaleFormatPreference) { - regionalFormat = "en" // default to "en", not "en-US", matching the regionalFormat code + regionalFormat = "en" + + // We default the regional format (locale) to the Accept-Language header rather than the language preference + // mainly because we want to avoid defaulting to en-US for most users who have not set a preference, and we + // don't have more specific English language preferences yet. + + // Regional format preference order (from most-preferred to least): + // 1. URL parameter + // 2. regionalFormat User preference + // 3. Accept-Language header + // 4. Language preference if urlPrefs.RegionalFormat != "" { regionalFormat = urlPrefs.RegionalFormat } else if prefs.JSONData.RegionalFormat != "" { regionalFormat = prefs.JSONData.RegionalFormat + } else if acceptLangHeaderFirstValue != "" { + regionalFormat = acceptLangHeaderFirstValue + } else if language != "" { + regionalFormat = language } } From ba2804bcfe703d96e6907af7f5ec14a6307ea7b7 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 23 Jul 2025 09:23:16 +0100 Subject: [PATCH 005/101] I18n: Use locale-aware date formatting in grafana-data (#108415) * use intl locale-aware date formatting * format range for time picker button * comment * fix test setup --- .../src/datetime/formatter.test.ts | 151 +++++++++++---- .../grafana-data/src/datetime/formatter.ts | 100 +++++++++- .../src/datetime/rangeutil.test.ts | 180 ++++++++++++++++++ .../grafana-data/src/datetime/rangeutil.ts | 34 +++- .../grafana-data/src/utils/featureToggles.ts | 13 ++ packages/grafana-i18n/src/dates.ts | 12 +- packages/grafana-i18n/src/index.ts | 2 +- .../DateTimePickers/TimeRangePicker/mapper.ts | 1 + 8 files changed, 451 insertions(+), 42 deletions(-) create mode 100644 packages/grafana-data/src/utils/featureToggles.ts diff --git a/packages/grafana-data/src/datetime/formatter.test.ts b/packages/grafana-data/src/datetime/formatter.test.ts index 18bea1cde78..7ec7da8adc5 100644 --- a/packages/grafana-data/src/datetime/formatter.test.ts +++ b/packages/grafana-data/src/datetime/formatter.test.ts @@ -1,5 +1,90 @@ +import { initRegionalFormatForTests } from '@grafana/i18n'; + +import * as featureToggles from '../utils/featureToggles'; + import { dateTimeFormat, dateTimeFormatTimeAgo, dateTimeFormatWithAbbrevation, timeZoneAbbrevation } from './formatter'; +// Default time zone ("browser") is set to Pacific/Easter in jest.config.js +const referenceDate = '2020-04-17T12:36:15.779Z'; + +describe('dateTimeFormat (regionalFormatPreference)', () => { + let mockGetFeatureToggle: jest.SpyInstance; + + beforeAll(() => { + initRegionalFormatForTests('en-AU'); + mockGetFeatureToggle = jest.spyOn(featureToggles, 'getFeatureToggle').mockImplementation((featureName) => { + return featureName === 'localeFormatPreference'; + }); + }); + + afterAll(() => { + mockGetFeatureToggle.mockRestore(); + }); + + it('formats dates in the browsers timezone by default', () => { + expect(dateTimeFormat(referenceDate)).toBe('17/04/2020, 6:36:15 am'); + }); + + it('formats dates in the browsers timezone by default when invalid time zone is set', () => { + const options = { timeZone: 'asdf123' }; + expect(dateTimeFormat(referenceDate, options)).toBe('17/04/2020, 6:36:15 am'); + }); + + it.each([ + ['UTC', '17/04/2020, 12:36:15 pm'], + ['Europe/Stockholm', '17/04/2020, 2:36:15 pm'], + ['Australia/Perth', '17/04/2020, 8:36:15 pm'], + ['Asia/Yakutsk', '17/04/2020, 9:36:15 pm'], + ['America/Panama', '17/04/2020, 7:36:15 am'], + ['America/Los_Angeles', '17/04/2020, 5:36:15 am'], + ['Africa/Djibouti', '17/04/2020, 3:36:15 pm'], + ['Europe/London', '17/04/2020, 1:36:15 pm'], + ['Europe/Berlin', '17/04/2020, 2:36:15 pm'], + ['Europe/Moscow', '17/04/2020, 3:36:15 pm'], + ['Europe/Madrid', '17/04/2020, 2:36:15 pm'], + ['America/New_York', '17/04/2020, 8:36:15 am'], + ['America/Chicago', '17/04/2020, 7:36:15 am'], + ['America/Denver', '17/04/2020, 6:36:15 am'], + ])('formats with the supplied time zone %s', (timeZone, expected) => { + expect(dateTimeFormat(referenceDate, { timeZone })).toBe(expected); + }); + + it('does not include seconds in the output if the time does not', () => { + expect(dateTimeFormat('2020-04-17T12:36:00.000Z')).toBe('17/04/2020, 6:36 am'); + }); + + it("includes milliseconds in the output when 'defaultWithMS' is set to true", () => { + expect(dateTimeFormat('2020-04-17T12:36:15.123Z', { defaultWithMS: true })).toBe('17/04/2020, 6:36:15.123 am'); + }); + + it.each([ + ['en-GB', '17/04/2020, 06:36:15'], + ['en-US', '4/17/2020, 6:36:15 AM'], + ['fr-FR', '17/04/2020 06:36:15'], + ['es-ES', '17/4/2020, 6:36:15'], + ['de-DE', '17.4.2020, 06:36:15'], + ['pt-BR', '17/04/2020, 06:36:15'], + ['zh-Hans', '2020/4/17 06:36:15'], + ['it-IT', '17/04/2020, 06:36:15'], + ['ja-JP', '2020/4/17 6:36:15'], + ['id-ID', '17/4/2020, 06.36.15'], + ['ko-KR', '2020. 4. 17. 오전 6:36:15'], + ['ru-RU', '17.04.2020, 06:36:15'], + ['cs-CZ', '17. 4. 2020 6:36:15'], + ['nl-NL', '17-4-2020, 06:36:15'], + ['hu-HU', '2020. 04. 17. 6:36:15'], + ['pt-PT', '17/04/2020, 06:36:15'], + ['pl-PL', '17.04.2020, 06:36:15'], + ['sv-SE', '2020-04-17 06:36:15'], + ['tr-TR', '17.04.2020 06:36:15'], + ['zh-Hant', '2020/4/17 上午6:36:15'], + ])('with locale %s', (locale, expected) => { + initRegionalFormatForTests(locale); + + expect(dateTimeFormat(referenceDate)).toBe(expected); + }); +}); + describe('dateTimeFormat', () => { describe('when no time zone have been set', () => { const browserTime = dateTimeFormat(1587126975779, { timeZone: 'browser' }); @@ -81,7 +166,7 @@ describe('dateTimeFormat', () => { }); }); - describe('DateTimeFormatISO', () => { + describe('with custom ISO format', () => { it('should format according to ISO standard', () => { const options = { timeZone: 'Europe/Stockholm', format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }; expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17T14:36:15.779+02:00'); @@ -95,39 +180,41 @@ describe('dateTimeFormat', () => { expect(dateTimeFormat(1587126975779, options)).toBe('2020-04-17T14:36:15.779+02:00'); }); }); +}); - describe('dateTimeFormatTimeAgo', () => { - it('should return the correct format for years ago', () => { - const options = { timeZone: 'Europe/Stockholm' }; - expect(dateTimeFormatTimeAgo(1587126975779, options)).toContain('years ago'); - }); +describe('dateTimeFormatTimeAgo', () => { + it('should return the correct format for years ago', () => { + const options = { timeZone: 'Europe/Stockholm' }; + expect(dateTimeFormatTimeAgo(1587126975779, options)).toContain('years ago'); }); - describe('dateTimeFormatWithAbbreviation', () => { - it('should return the correct format with zone abbreviation', () => { - const options = { timeZone: 'Europe/Stockholm' }; - expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 14:36:15 CEST'); - }); - it('should return the correct format with zone abbreviation', () => { - const options = { timeZone: 'America/New_York' }; - expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 08:36:15 EDT'); - }); - it('should return the correct format with zone abbreviation', () => { - const options = { timeZone: 'Europe/Bucharest' }; - expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 15:36:15 EEST'); - }); +}); + +describe('dateTimeFormatWithAbbreviation', () => { + it('should return the correct format with zone abbreviation', () => { + const options = { timeZone: 'Europe/Stockholm' }; + expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 14:36:15 CEST'); }); - describe('timeZoneAbbrevation', () => { - it('should return the correct abbreviation', () => { - const options = { timeZone: 'Europe/Stockholm' }; - expect(timeZoneAbbrevation(1587126975779, options)).toBe('CEST'); - }); - it('should return the correct abbreviation', () => { - const options = { timeZone: 'America/New_York' }; - expect(timeZoneAbbrevation(1587126975779, options)).toBe('EDT'); - }); - it('should return the correct abbreviation', () => { - const options = { timeZone: 'Europe/Bucharest' }; - expect(timeZoneAbbrevation(1587126975779, options)).toBe('EEST'); - }); + it('should return the correct format with zone abbreviation', () => { + const options = { timeZone: 'America/New_York' }; + expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 08:36:15 EDT'); + }); + it('should return the correct format with zone abbreviation', () => { + const options = { timeZone: 'Europe/Bucharest' }; + expect(dateTimeFormatWithAbbrevation(1587126975779, options)).toBe('2020-04-17 15:36:15 EEST'); + }); +}); + +describe('timeZoneAbbrevation', () => { + it('should return the correct abbreviation', () => { + const options = { timeZone: 'Europe/Stockholm' }; + expect(timeZoneAbbrevation(1587126975779, options)).toBe('CEST'); + }); + it('should return the correct abbreviation', () => { + const options = { timeZone: 'America/New_York' }; + expect(timeZoneAbbrevation(1587126975779, options)).toBe('EDT'); + }); + it('should return the correct abbreviation', () => { + const options = { timeZone: 'Europe/Bucharest' }; + expect(timeZoneAbbrevation(1587126975779, options)).toBe('EEST'); }); }); diff --git a/packages/grafana-data/src/datetime/formatter.ts b/packages/grafana-data/src/datetime/formatter.ts index 7facb730195..cc90e27491a 100644 --- a/packages/grafana-data/src/datetime/formatter.ts +++ b/packages/grafana-data/src/datetime/formatter.ts @@ -1,12 +1,77 @@ /* eslint-disable id-blacklist, no-restricted-imports */ import moment, { Moment } from 'moment-timezone'; +import { formatDate } from '@grafana/i18n'; + import { TimeZone } from '../types/time'; +import { getFeatureToggle } from '../utils/featureToggles'; import { DateTimeOptions, getTimeZone } from './common'; import { systemDateFormats } from './formats'; import { DateTimeInput, toUtc, dateTimeAsMoment } from './moment_wrapper'; +/** + * Converts a Grafana DateTimeInput to a plain Javascript Date object. + */ +function toDate(dateInUtc: DateTimeInput): Date { + if (dateInUtc instanceof Date) { + return dateInUtc; + } + + if (typeof dateInUtc === 'string' || typeof dateInUtc === 'number') { + return new Date(dateInUtc); + } + + return dateTimeAsMoment(dateInUtc).toDate(); +} + +/** + * Converts a Grafana timezone string to an IANA timezone string. + */ +export function toIANATimezone(grafanaTimezone: string) { + // Intl APIs will use the browser's timezone by default (if tz is undefined) + if (grafanaTimezone === 'browser') { + return undefined; + } + + const zone = moment.tz.zone(grafanaTimezone); + if (!zone) { + // If the timezone is invalid, we default to the browser's timezone + return undefined; + } + + return grafanaTimezone; +} + +function getIntlOptions( + date: Date, + options?: DateTimeOptionsWithFormat +): Intl.DateTimeFormatOptions & { timeZone?: string } { + const timeZone = getTimeZone(options); + + const intlOptions: Intl.DateTimeFormatOptions = { + year: 'numeric', // ↔ dateStyle: 'short' + month: 'numeric', + day: 'numeric', + hour: 'numeric', // ↔ timeStyle: 'short' + minute: 'numeric', + timeZone: toIANATimezone(timeZone), + }; + + // If the time has seconds, ensure they're included in the format + const hasSeconds = date.getSeconds() !== 0; + if (hasSeconds) { + intlOptions.second = 'numeric'; + } + + if (options?.defaultWithMS) { + intlOptions.second = 'numeric'; + intlOptions.fractionalSecondDigits = 3; // Include milliseconds + } + + return intlOptions; +} + /** * The type describing the options that can be passed to the {@link dateTimeFormat} * helper function to control how the date and time value passed to the function is @@ -23,17 +88,30 @@ export interface DateTimeOptionsWithFormat extends DateTimeOptions { type DateTimeFormatter = (dateInUtc: DateTimeInput, options?: T) => string; +// NOTE: +// These date formatting functions now just wrap the @grafana/i18n formatting functions +// (which themselves wrap the browserIntl APIs). In the future we may deprecate these +// in favor of using @grafana/i18n directly. + /** - * Helper function to format date and time according to the specified options. If no options - * are supplied, then default values are used. For more details, see {@link DateTimeOptionsWithFormat}. + * Helper function to format date and time according to the specified options. + * If no options are supplied, then the date is formatting according to the user's locale preference. * * @param dateInUtc - date in UTC format, e.g. string formatted with UTC offset, UNIX epoch in seconds etc. * @param options * * @public */ -export const dateTimeFormat: DateTimeFormatter = (dateInUtc, options?) => - toTz(dateInUtc, getTimeZone(options)).format(getFormat(options)); +export const dateTimeFormat: DateTimeFormatter = (dateInUtc, options?) => { + // If a custom format is provided (or the toggle isn't enabled), use the previous implementation + if (!getFeatureToggle('localeFormatPreference') || options?.format) { + return toTz(dateInUtc, getTimeZone(options)).format(getFormat(options)); + } + + const dateAsDate = toDate(dateInUtc); + const intlOptions = getIntlOptions(dateAsDate, options); // TODO - if invalid timezone, use browser timezone + return formatDate(dateAsDate, intlOptions); +}; /** * Helper function to format date and time according to the standard ISO format e.g. 2013-02-04T22:44:30.652Z. @@ -70,8 +148,18 @@ export const dateTimeFormatTimeAgo: DateTimeFormatter = (dateInUtc, options?) => * * @public */ -export const dateTimeFormatWithAbbrevation: DateTimeFormatter = (dateInUtc, options?) => - toTz(dateInUtc, getTimeZone(options)).format(`${systemDateFormats.fullDate} z`); +export const dateTimeFormatWithAbbrevation: DateTimeFormatter = (dateInUtc, options?) => { + // If a custom format is provided (or the toggle isn't enabled), use the previous implementation + if (!getFeatureToggle('localeFormatPreference') || options?.format) { + return toTz(dateInUtc, getTimeZone(options)).format(`${systemDateFormats.fullDate} z`); + } + + const dateAsDate = toDate(dateInUtc); + const intlOptions = getIntlOptions(dateAsDate, options); + intlOptions.timeZoneName = 'short'; + + return formatDate(dateAsDate, intlOptions); +}; /** * Helper function to return only the time zone abbreviation for a given date and time value. If no options diff --git a/packages/grafana-data/src/datetime/rangeutil.test.ts b/packages/grafana-data/src/datetime/rangeutil.test.ts index 684e4a11935..81ac6e2f6f8 100644 --- a/packages/grafana-data/src/datetime/rangeutil.test.ts +++ b/packages/grafana-data/src/datetime/rangeutil.test.ts @@ -1,9 +1,13 @@ +import { initRegionalFormatForTests } from '@grafana/i18n'; + import { RawTimeRange, TimeRange } from '../types/time'; +import * as featureToggles from '../utils/featureToggles'; import { dateTime } from './moment_wrapper'; import { convertRawToRange, describeInterval, + describeTimeRange, isRelativeTimeRange, relativeToTimeRange, roundInterval, @@ -310,4 +314,180 @@ describe('Range Utils', () => { expect(relativeTimeRange.to).toEqual(604800); }); }); + + describe('describeTimeRange', () => { + it.each([ + ['now-5m', 'now', 'Last 5 minutes'], + ['now-15m', 'now', 'Last 15 minutes'], + ['now-1h', 'now', 'Last 1 hour'], + ['now-24h', 'now', 'Last 24 hours'], + ['now-7d', 'now', 'Last 7 days'], + ['now-30d', 'now', 'Last 30 days'], + ['now/d', 'now/d', 'Today'], + ['now/d', 'now', 'Today so far'], + ['now/w', 'now/w', 'This week'], + ['now/M', 'now/M', 'This month'], + ['now/y', 'now/y', 'This year'], + ['now-1d/d', 'now-1d/d', 'Yesterday'], + ['now-1w/w', 'now-1w/w', 'Previous week'], + ['now-1M/M', 'now-1M/M', 'Previous month'], + ['now-1y/y', 'now-1y/y', 'Previous year'], + ['now/fQ', 'now/fQ', 'This fiscal quarter'], + ['now/fy', 'now/fy', 'This fiscal year'], + ['now-1Q/fQ', 'now-1Q/fQ', 'Previous fiscal quarter'], + ['now-1y/fy', 'now-1y/fy', 'Previous fiscal year'], + ])('should return display name "%s" for range from "%s" to "%s"', (from, to, expected) => { + expect(describeTimeRange({ from, to })).toBe(expected); + }); + + it('should prioritize custom quick ranges over standard ranges', () => { + const customRanges = [{ from: 'now-5m', to: 'now', display: 'Lightning round!' }]; + + expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, customRanges)).toBe('Lightning round!'); + }); + + it('should format with the default timezone', () => { + // Tests default to Pacific/Easter + const from = dateTime('2023-01-15T10:30:00Z'); + const to = dateTime('2023-01-15T14:45:00Z'); + + const result = describeTimeRange({ from, to }); + expect(result).toBe('2023-01-15 05:30:00 to 2023-01-15 09:45:00'); + }); + + it('should respect timezone in datetime formatting', () => { + const from = dateTime('2023-01-15T10:30:00Z'); + const to = dateTime('2023-01-15T14:45:00Z'); + + const result = describeTimeRange({ from, to }, 'Australia/Sydney'); + expect(result).toBe('2023-01-15 21:30:00 to 2023-01-16 01:45:00'); + }); + + it('should handle absolute from, relative to', () => { + const from = dateTime('2023-01-15T10:30:00Z'); + const to = 'now'; + + const result = describeTimeRange({ from, to }); + expect(result).toBe('2023-01-15 05:30:00 to a few seconds ago'); + }); + + it('should handle relative from, absolute to', () => { + const from = 'now-1h'; + const to = dateTime('2023-01-15T14:45:00Z'); + + const result = describeTimeRange({ from, to }); + expect(result).toBe('an hour ago to 2023-01-15 09:45:00'); + }); + + it('should handle invalid relative expressions', () => { + const from = dateTime('2023-01-15T10:30:00Z'); + const to = 'invalid-expression'; + + const result = describeTimeRange({ from, to }); + expect(result).toContain('Invalid date'); + }); + + it('should handle empty custom ranges array', () => { + expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, [])).toBe('Last 5 minutes'); + }); + + it('should handle null custom ranges', () => { + expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, undefined)).toBe('Last 5 minutes'); + }); + }); + + describe('describeTimeRange - localeFormatPreference enabled', () => { + let mockGetFeatureToggle: jest.SpyInstance; + + beforeAll(() => { + initRegionalFormatForTests('en-AU'); + mockGetFeatureToggle = jest.spyOn(featureToggles, 'getFeatureToggle').mockImplementation((featureName) => { + return featureName === 'localeFormatPreference'; + }); + }); + + afterAll(() => { + mockGetFeatureToggle.mockRestore(); + }); + + it.each([ + ['now-5m', 'now', 'Last 5 minutes'], + ['now-15m', 'now', 'Last 15 minutes'], + ['now-1h', 'now', 'Last 1 hour'], + ['now-7d', 'now', 'Last 7 days'], + ['now/d', 'now/d', 'Today'], + ['now/d', 'now', 'Today so far'], + ['now/w', 'now/w', 'This week'], + ['now-1d/d', 'now-1d/d', 'Yesterday'], + ['now-1w/w', 'now-1w/w', 'Previous week'], + ['now-1y/y', 'now-1y/y', 'Previous year'], + ['now/fQ', 'now/fQ', 'This fiscal quarter'], + ['now-1Q/fQ', 'now-1Q/fQ', 'Previous fiscal quarter'], + ])('should return display name "%s" for range from "%s" to "%s"', (from, to, expected) => { + expect(describeTimeRange({ from, to })).toBe(expected); + }); + + it('should prioritize custom quick ranges over standard ranges', () => { + const customRanges = [{ from: 'now-5m', to: 'now', display: 'Lightning round!' }]; + + expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, customRanges)).toBe('Lightning round!'); + }); + + it('should format with the default timezone', () => { + // Tests default to Pacific/Easter + const from = dateTime('2023-01-15T10:30:00Z'); + const to = dateTime('2023-01-15T14:45:00Z'); + + const result = describeTimeRange({ from, to }); + expect(result).toBe('15/1/23, 5:30 – 9:45 am'); + }); + + it('should respect timezone in datetime formatting', () => { + const from = dateTime('2023-01-15T10:30:00Z'); + const to = dateTime('2023-01-15T14:45:00Z'); + + const result = describeTimeRange({ from, to }, 'Australia/Sydney'); + expect(result).toBe('15/1/23, 9:30 pm – 16/1/23, 1:45 am'); + }); + + it('should include seconds in the output if the time has seconds', () => { + const from = dateTime('2023-01-15T10:30:00Z'); + const to = dateTime('2023-01-15T14:45:12Z'); + + const result = describeTimeRange({ from, to }); + expect(result).toBe('15/1/23, 5:30:00 am – 9:45:12 am'); + }); + + it('should handle absolute from, relative to', () => { + const from = dateTime('2023-01-15T10:30:00Z'); + const to = 'now'; + + const result = describeTimeRange({ from, to }); + expect(result).toBe('15/01/2023, 5:30 am to a few seconds ago'); + }); + + it('should handle relative from, absolute to', () => { + const from = 'now-1h'; + const to = dateTime('2023-01-15T14:45:00Z'); + + const result = describeTimeRange({ from, to }); + expect(result).toBe('an hour ago to 15/01/2023, 9:45 am'); + }); + + it('should handle invalid relative expressions', () => { + const from = dateTime('2023-01-15T10:30:00Z'); + const to = 'invalid-expression'; + + const result = describeTimeRange({ from, to }); + expect(result).toContain('Invalid date'); + }); + + it('should handle empty custom ranges array', () => { + expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, [])).toBe('Last 5 minutes'); + }); + + it('should handle null custom ranges', () => { + expect(describeTimeRange({ from: 'now-5m', to: 'now' }, undefined, undefined)).toBe('Last 5 minutes'); + }); + }); }); diff --git a/packages/grafana-data/src/datetime/rangeutil.ts b/packages/grafana-data/src/datetime/rangeutil.ts index 2a93c282551..680ce22dcf0 100644 --- a/packages/grafana-data/src/datetime/rangeutil.ts +++ b/packages/grafana-data/src/datetime/rangeutil.ts @@ -1,7 +1,10 @@ +import { formatDateRange } from '@grafana/i18n'; + import { RawTimeRange, TimeRange, TimeZone, IntervalValues, RelativeTimeRange, TimeOption } from '../types/time'; +import { getFeatureToggle } from '../utils/featureToggles'; import * as dateMath from './datemath'; -import { timeZoneAbbrevation, dateTimeFormat, dateTimeFormatTimeAgo } from './formatter'; +import { timeZoneAbbrevation, dateTimeFormat, dateTimeFormatTimeAgo, toIANATimezone } from './formatter'; import { isDateTime, DateTime, dateTime } from './moment_wrapper'; import { dateTimeParse } from './parser'; @@ -134,6 +137,17 @@ export function describeTextRange(expr: string): TimeOption { return opt; } +// TODO: Should we keep these format presets somewhere common? +const rangeFormatShort: Intl.DateTimeFormatOptions = { + dateStyle: 'short', + timeStyle: 'short', +}; + +const rangeFormatFull: Intl.DateTimeFormatOptions = { + dateStyle: 'short', + timeStyle: 'medium', +}; + /** * Use this function to get a properly formatted string representation of a {@link @grafana/data:RawTimeRange | range}. * @@ -154,9 +168,25 @@ export function describeTimeRange(range: RawTimeRange, timeZone?: TimeZone, quic const options = { timeZone }; if (isDateTime(range.from) && isDateTime(range.to)) { - return dateTimeFormat(range.from, options) + ' to ' + dateTimeFormat(range.to, options); + const fromDate = range.from.toDate(); + const toDate = range.to.toDate(); + + if (!getFeatureToggle('localeFormatPreference')) { + return dateTimeFormat(range.from, options) + ' to ' + dateTimeFormat(range.to, options); + } + + const hasSeconds = fromDate.getSeconds() !== 0 || toDate.getSeconds() !== 0; + const intlFormat = hasSeconds ? rangeFormatFull : rangeFormatShort; + const intlFormatOptions = { + ...intlFormat, + timeZone: timeZone ? toIANATimezone(timeZone) : undefined, + }; + + return formatDateRange(fromDate, toDate, intlFormatOptions); } + // TODO: We could update these to all use Intl APIs. + // Could we use formatRangeToParts and replace the 'other side' with the ago formatting? if (isDateTime(range.from)) { const parsed = dateMath.parse(range.to, true, 'utc'); return parsed ? dateTimeFormat(range.from, options) + ' to ' + dateTimeFormatTimeAgo(parsed, options) : ''; diff --git a/packages/grafana-data/src/utils/featureToggles.ts b/packages/grafana-data/src/utils/featureToggles.ts new file mode 100644 index 00000000000..125c8fa8536 --- /dev/null +++ b/packages/grafana-data/src/utils/featureToggles.ts @@ -0,0 +1,13 @@ +import { FeatureToggles } from '../types/featureToggles.gen'; + +type FeatureToggleName = keyof FeatureToggles; + +/** + * Check a featureToggle + * @param featureName featureToggle name + * @param def default value if featureToggles aren't defined, false if not provided + * @returns featureToggle value or def. + */ +export function getFeatureToggle(featureName: FeatureToggleName, def = false) { + return window.grafanaBootData?.settings.featureToggles[featureName] ?? def; +} diff --git a/packages/grafana-i18n/src/dates.ts b/packages/grafana-i18n/src/dates.ts index db699e871ae..c75cd2c86f4 100644 --- a/packages/grafana-i18n/src/dates.ts +++ b/packages/grafana-i18n/src/dates.ts @@ -1,8 +1,13 @@ import deepEqual from 'fast-deep-equal'; -import memoize from 'micro-memoize'; +import memoize, { AnyFn, Memoized } from 'micro-memoize'; const deepMemoize: typeof memoize = (fn) => memoize(fn, { isEqual: deepEqual }); +function clearMemoizedCache(fn: Memoized) { + fn.cache.keys.length = 0; + fn.cache.values.length = 0; +} + let regionalFormat: string | undefined; const createDateTimeFormatter = deepMemoize((locale: string | undefined, options: Intl.DateTimeFormatOptions) => { @@ -41,5 +46,10 @@ export const formatDateRange = ( }; export const initRegionalFormat = (regionalFormatArg: string) => { + // We don't expect this to be called with a different locale during the lifetime of the app, + // so this is mostly here so we can change it during tests and clear out previously memoized values. + clearMemoizedCache(formatDate); + clearMemoizedCache(formatDuration); + regionalFormat = regionalFormatArg; }; diff --git a/packages/grafana-i18n/src/index.ts b/packages/grafana-i18n/src/index.ts index a4ebd2b5289..8a83bb17c1b 100644 --- a/packages/grafana-i18n/src/index.ts +++ b/packages/grafana-i18n/src/index.ts @@ -24,4 +24,4 @@ export { } from './constants'; export { initPluginTranslations, t, Trans } from './i18n'; export type { ResourceLoader, Resources, TFunction, TransProps } from './types'; -export { formatDate, formatDuration, formatDateRange } from './dates'; +export { formatDate, formatDuration, formatDateRange, initRegionalFormat as initRegionalFormatForTests } from './dates'; diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/mapper.ts b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/mapper.ts index cce0bb76e37..160a1bdbc96 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/mapper.ts +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/mapper.ts @@ -7,6 +7,7 @@ export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): T return rangeUtil.convertRawToRange({ from: option.from, to: option.to }, timeZone, undefined, commonFormat); }; +// TODO: Should we keep these format presets somewhere common? const rangeFormatShort: Intl.DateTimeFormatOptions = { dateStyle: 'short', timeStyle: 'short', From f093b3d62070665bfb8c626382ecf9698aae7a79 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 23 Jul 2025 10:03:34 +0100 Subject: [PATCH 006/101] Chore: Ignore cypress updates (#108496) ignore cypress updates for now --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index dc1677fc28b..37d44d74483 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -12,6 +12,7 @@ 'react-refresh', "@types/history", // this can be removed entirely when we upgrade history since v5 exposes types directly + "cypress", // cypress use is deprecated, we should not bump it anymore "history", // we should bump this together with react-router-dom (see https://github.com/grafana/grafana/issues/76744) "react-router", // we should bump this together with history and react-router-dom "react-router-dom", // we should bump this together with history (see https://github.com/grafana/grafana/issues/76744) From 937cdc3682fb167bbd752963fc2e627836939eb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:31:57 +0100 Subject: [PATCH 007/101] Update dependency cypress-recurse to v1.37.1 (#108498) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 84667f53164..866a76ae882 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14030,11 +14030,11 @@ __metadata: linkType: hard "cypress-recurse@npm:^1.35.3": - version: 1.35.3 - resolution: "cypress-recurse@npm:1.35.3" + version: 1.37.1 + resolution: "cypress-recurse@npm:1.37.1" dependencies: humanize-duration: "npm:^3.27.3" - checksum: 10/408781ff538c0ac832bf1ce61b3e6628a6ab5832d9c425e06c4d724eed977dcadec03f234d6f90ea1826f21d440d5f7f971738399770b83358918be16da53d75 + checksum: 10/fd2bbd1613b5cf7dc2127c813f3a700eccad6319ee3d011a61a373591d491eb202fe0113faa5156c1e1d2d9f2663a41ab49240abf842dff217d70854e668316c languageName: node linkType: hard From 3fe21f1ccab0cdcc387be605c1e127b412358eac Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:32:30 +0200 Subject: [PATCH 008/101] Alerting: Add pagination for rules in a group in the rule list view v2 (#108436) * add pagination for rules in a group, in the rule list view v2 * update translations * remove gap in stack --- .../unified/rule-list/GrafanaGroupLoader.tsx | 60 +++++++++++++++++-- public/locales/en-US/grafana.json | 3 +- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx index 2dee977835d..7d9a8d184de 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.tsx @@ -1,13 +1,20 @@ +import { css } from '@emotion/css'; +import { useMemo } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { Alert } from '@grafana/ui'; +import { Alert, Pagination, Stack, useStyles2 } from '@grafana/ui'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; import { prometheusApi } from '../api/prometheusApi'; +import { usePagination } from '../hooks/usePagination'; import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; import { GrafanaRuleListItem } from './GrafanaRuleListItem'; import { AlertRuleListItemSkeleton } from './components/AlertRuleListItemLoader'; +const DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP_LIST_VIEW_V2 = 100; + const { useGetGrafanaGroupsQuery } = prometheusApi; export interface GrafanaGroupLoaderProps { @@ -31,6 +38,8 @@ export function GrafanaGroupLoader({ namespaceName, expectedRulesCount = 3, // 3 is a random number. Usually we get the number of rules from Prometheus response }: GrafanaGroupLoaderProps) { + const styles = useStyles2(getStyles); + const { data: promResponse, isLoading: isPromResponseLoading } = useGetGrafanaGroupsQuery( { folderUid: groupIdentifier.namespace.uid, @@ -40,6 +49,16 @@ export function GrafanaGroupLoader({ { pollingInterval: RULE_LIST_POLL_INTERVAL_MS } ); + const rules = useMemo(() => { + return promResponse?.data.groups.at(0)?.rules ?? []; + }, [promResponse]); + + const { pageItems, page, numberOfPages, onPageChange } = usePagination( + rules, + 1, + DEFAULT_PER_PAGE_PAGINATION_RULES_PER_GROUP_LIST_VIEW_V2 + ); + if (isPromResponseLoading) { return ( <> @@ -63,9 +82,14 @@ export function GrafanaGroupLoader({ ); } + // If no rules found, return early without pagination + if (rules.length === 0) { + return ; + } + return ( - <> - {promResponse.data.groups.at(0)?.rules.map((promRule) => { + + {pageItems.map((promRule) => { return ( ); })} - +
+ {numberOfPages > 1 && ( + + )} +
+
); } + +const getStyles = (theme: GrafanaTheme2) => ({ + pagination: css({ + display: 'flex', + margin: 0, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(0.25), + justifyContent: 'center', + float: 'none', + }), + paginationWrapper: css({ + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + marginLeft: theme.spacing(2.5), + }), +}); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2c009a575c4..42438db155f 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1551,7 +1551,8 @@ "title": "Edit evaluation group" }, "group-loader": { - "group-load-failed": "Failed to load rules from group {{ groupName }} in {{ namespaceName }}" + "group-load-failed": "Failed to load rules from group {{ groupName }} in {{ namespaceName }}", + "no-rules": "No rules found in this group" }, "group-status": { "content-the-group-is-being-deleted": "The group is being deleted" From 946358900c43b470d640230d73b0624aa93239de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Wed, 23 Jul 2025 11:33:25 +0200 Subject: [PATCH 009/101] Provisioning: Add warning job status and different strictness (#108446) * Add warning job status * Different strictness for export than for pulling * Fix some unit tests * Fix unit tests * Update frontend to display the warning status * Fix status badge * Add missing return statement * Improve text in alert --- pkg/apis/provisioning/v0alpha1/jobs.go | 5 ++- .../v0alpha1/zz_generated.openapi.go | 8 ++--- .../jobs/job_progress_recorder_mock.go | 27 ++++++++-------- .../apis/provisioning/jobs/migrate/legacy.go | 4 +++ .../provisioning/jobs/migrate/legacy_test.go | 31 ++++++++++++++++--- .../jobs/migrate/unifiedstorage.go | 2 ++ .../jobs/migrate/unifiedstorage_test.go | 4 +++ .../apis/provisioning/jobs/migrate/worker.go | 1 - .../provisioning/jobs/migrate/worker_test.go | 6 ---- .../apis/provisioning/jobs/progress.go | 18 ++++++----- .../provisioning/jobs/progress_fn_mock.go | 2 +- pkg/registry/apis/provisioning/jobs/queue.go | 2 +- .../provisioning/jobs/repo_getter_mock.go | 2 +- .../apis/provisioning/jobs/sync/worker.go | 2 ++ .../provisioning/jobs/sync/worker_test.go | 7 +++++ .../apis/provisioning/jobs/worker_mock.go | 2 +- .../apis/provisioning/resources/resources.go | 3 +- .../provisioning.grafana.app-v0alpha1.json | 6 ++-- .../provisioning/v0alpha1/endpoints.gen.ts | 6 ++-- .../provisioning/Job/FinishedJobStatus.tsx | 3 ++ .../features/provisioning/Job/JobContent.tsx | 11 ++++++- .../features/provisioning/Job/RecentJobs.tsx | 5 ++- .../provisioning/Shared/StatusBadge.tsx | 9 ++++-- public/locales/en-US/grafana.json | 3 +- 24 files changed, 119 insertions(+), 50 deletions(-) diff --git a/pkg/apis/provisioning/v0alpha1/jobs.go b/pkg/apis/provisioning/v0alpha1/jobs.go index f9801049cf4..f4fa20e8934 100644 --- a/pkg/apis/provisioning/v0alpha1/jobs.go +++ b/pkg/apis/provisioning/v0alpha1/jobs.go @@ -54,10 +54,13 @@ const ( // Finished with errors JobStateError JobState = "error" + + // Finished with some non-critical errors + JobStateWarning JobState = "warning" ) func (j JobState) Finished() bool { - return j == JobStateSuccess || j == JobStateError + return j == JobStateSuccess || j == JobStateError || j == JobStateWarning } type JobSpec struct { diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index cdac15c0c6f..58a67095b53 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -890,10 +890,10 @@ func schema_pkg_apis_provisioning_v0alpha1_JobStatus(ref common.ReferenceCallbac Properties: map[string]spec.Schema{ "state": { SchemaProps: spec.SchemaProps{ - Description: "Possible enum values:\n - `\"error\"` Finished with errors\n - `\"pending\"` Job has been submitted, but not processed yet\n - `\"success\"` Finished with success\n - `\"working\"` The job is running", + Description: "Possible enum values:\n - `\"error\"` Finished with errors\n - `\"pending\"` Job has been submitted, but not processed yet\n - `\"success\"` Finished with success\n - `\"warning\"` Finished with some non-critical errors\n - `\"working\"` The job is running", Type: []string{"string"}, Format: "", - Enum: []interface{}{"error", "pending", "success", "working"}, + Enum: []interface{}{"error", "pending", "success", "warning", "working"}, }, }, "started": { @@ -2135,11 +2135,11 @@ func schema_pkg_apis_provisioning_v0alpha1_SyncStatus(ref common.ReferenceCallba Properties: map[string]spec.Schema{ "state": { SchemaProps: spec.SchemaProps{ - Description: "pending, running, success, error\n\nPossible enum values:\n - `\"error\"` Finished with errors\n - `\"pending\"` Job has been submitted, but not processed yet\n - `\"success\"` Finished with success\n - `\"working\"` The job is running", + Description: "pending, running, success, error\n\nPossible enum values:\n - `\"error\"` Finished with errors\n - `\"pending\"` Job has been submitted, but not processed yet\n - `\"success\"` Finished with success\n - `\"warning\"` Finished with some non-critical errors\n - `\"working\"` The job is running", Default: "", Type: []string{"string"}, Format: "", - Enum: []interface{}{"error", "pending", "success", "working"}, + Enum: []interface{}{"error", "pending", "success", "warning", "working"}, }, }, "job": { diff --git a/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go b/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go index 7ead8a206e7..54b07e6a38a 100644 --- a/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go +++ b/pkg/registry/apis/provisioning/jobs/job_progress_recorder_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.52.4. DO NOT EDIT. package jobs @@ -237,34 +237,35 @@ func (_c *MockJobProgressRecorder_SetTotal_Call) RunAndReturn(run func(context.C return _c } -// Strict provides a mock function with no fields -func (_m *MockJobProgressRecorder) Strict() { - _m.Called() +// StrictMaxErrors provides a mock function with given fields: maxErrors +func (_m *MockJobProgressRecorder) StrictMaxErrors(maxErrors int) { + _m.Called(maxErrors) } -// MockJobProgressRecorder_Strict_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Strict' -type MockJobProgressRecorder_Strict_Call struct { +// MockJobProgressRecorder_StrictMaxErrors_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StrictMaxErrors' +type MockJobProgressRecorder_StrictMaxErrors_Call struct { *mock.Call } -// Strict is a helper method to define mock.On call -func (_e *MockJobProgressRecorder_Expecter) Strict() *MockJobProgressRecorder_Strict_Call { - return &MockJobProgressRecorder_Strict_Call{Call: _e.mock.On("Strict")} +// StrictMaxErrors is a helper method to define mock.On call +// - maxErrors int +func (_e *MockJobProgressRecorder_Expecter) StrictMaxErrors(maxErrors interface{}) *MockJobProgressRecorder_StrictMaxErrors_Call { + return &MockJobProgressRecorder_StrictMaxErrors_Call{Call: _e.mock.On("StrictMaxErrors", maxErrors)} } -func (_c *MockJobProgressRecorder_Strict_Call) Run(run func()) *MockJobProgressRecorder_Strict_Call { +func (_c *MockJobProgressRecorder_StrictMaxErrors_Call) Run(run func(maxErrors int)) *MockJobProgressRecorder_StrictMaxErrors_Call { _c.Call.Run(func(args mock.Arguments) { - run() + run(args[0].(int)) }) return _c } -func (_c *MockJobProgressRecorder_Strict_Call) Return() *MockJobProgressRecorder_Strict_Call { +func (_c *MockJobProgressRecorder_StrictMaxErrors_Call) Return() *MockJobProgressRecorder_StrictMaxErrors_Call { _c.Call.Return() return _c } -func (_c *MockJobProgressRecorder_Strict_Call) RunAndReturn(run func()) *MockJobProgressRecorder_Strict_Call { +func (_c *MockJobProgressRecorder_StrictMaxErrors_Call) RunAndReturn(run func(int)) *MockJobProgressRecorder_StrictMaxErrors_Call { _c.Run(run) return _c } diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go index 191e0c4922f..f2a78b5181e 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go @@ -41,6 +41,10 @@ func (m *LegacyMigrator) Migrate(ctx context.Context, rw repository.ReaderWriter Timeout: 10 * time.Minute, } + // Fail if migrating at least one + progress.StrictMaxErrors(1) + progress.SetMessage(ctx, "migrating legacy resources") + if err := m.wrapWithStageFn(ctx, rw, stageOptions, func(repo repository.Repository, staged bool) error { rw, ok := repo.(repository.ReaderWriter) if !ok { diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go index 877bd01fd33..3b25de7aeaf 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy_test.go @@ -34,6 +34,8 @@ func TestWrapWithStageFn(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() // Execute repo := repository.NewMockRepository(t) @@ -67,6 +69,8 @@ func TestWrapWithCloneFn_Error(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() // Execute repo := repository.NewMockRepository(t) repo.On("Config").Return(&provisioning.Repository{ @@ -109,6 +113,8 @@ func TestLegacyMigrator_MigrateFails(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() // Execute repo := repository.NewMockRepository(t) @@ -158,7 +164,9 @@ func TestLegacyMigrator_ResetUnifiedStorageFails(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() + progress.On("SetMessage", mock.Anything, "resetting unified storage").Return() // Execute repo := repository.NewMockRepository(t) @@ -213,8 +221,12 @@ func TestLegacyMigrator_SyncFails(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() + progress.On("SetMessage", mock.Anything, "resetting unified storage").Return() progress.On("ResetResults").Return() + progress.On("SetMessage", mock.Anything, "pulling resources").Return() + progress.On("SetMessage", mock.Anything, "error importing resources, reverting").Return() // Execute repo := repository.NewMockRepository(t) @@ -268,8 +280,12 @@ func TestLegacyMigrator_SyncFails(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() + progress.On("SetMessage", mock.Anything, "resetting unified storage").Return() progress.On("ResetResults").Return() + progress.On("SetMessage", mock.Anything, "pulling resources").Return() + progress.On("SetMessage", mock.Anything, "error importing resources, reverting").Return() // Execute repo := repository.NewMockRepository(t) @@ -321,8 +337,11 @@ func TestLegacyMigrator_Success(t *testing.T) { ) progress := jobs.NewMockJobProgressRecorder(t) - progress.On("SetMessage", mock.Anything, mock.Anything).Return() + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() + progress.On("SetMessage", mock.Anything, "resetting unified storage").Return() progress.On("ResetResults").Return() + progress.On("SetMessage", mock.Anything, "pulling resources").Return() // Execute repo := repository.NewMockRepository(t) @@ -364,6 +383,8 @@ func TestLegacyMigrator_BeforeFnExecution(t *testing.T) { progress := jobs.NewMockJobProgressRecorder(t) // No progress messages expected in current staging implementation + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() // Execute repo := repository.NewMockRepository(t) @@ -398,6 +419,8 @@ func TestLegacyMigrator_ProgressScanner(t *testing.T) { progress := jobs.NewMockJobProgressRecorder(t) // No progress messages expected in current staging implementation + progress.On("StrictMaxErrors", 1).Return() + progress.On("SetMessage", mock.Anything, "migrating legacy resources").Return() repo := repository.NewMockRepository(t) repo.On("Config").Return(&provisioning.Repository{ diff --git a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go index e2caeb7b3f4..d0b9739d72e 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go @@ -33,6 +33,8 @@ func NewUnifiedStorageMigrator( func (m *UnifiedStorageMigrator) Migrate(ctx context.Context, repo repository.ReaderWriter, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { namespace := repo.Config().GetNamespace() progress.SetMessage(ctx, "export resources") + progress.StrictMaxErrors(1) // strict as we want the entire instance to be managed + exportJob := provisioning.Job{ Spec: provisioning.JobSpec{ Push: &provisioning.ExportJobOptions{}, diff --git a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage_test.go b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage_test.go index 992f99cb299..3cb48119842 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage_test.go @@ -30,6 +30,7 @@ func TestUnifiedStorageMigrator_Migrate(t *testing.T) { }, }) pr.On("SetMessage", mock.Anything, "export resources").Return() + pr.On("StrictMaxErrors", 1).Return() ew.On("Process", mock.Anything, rw, mock.MatchedBy(func(job provisioning.Job) bool { return job.Spec.Push != nil }), pr).Return(errors.New("export failed")) @@ -46,6 +47,7 @@ func TestUnifiedStorageMigrator_Migrate(t *testing.T) { }, }) pr.On("SetMessage", mock.Anything, "export resources").Return() + pr.On("StrictMaxErrors", 1).Return() ew.On("Process", mock.Anything, rw, mock.MatchedBy(func(job provisioning.Job) bool { return job.Spec.Push != nil }), pr).Return(nil) @@ -67,6 +69,7 @@ func TestUnifiedStorageMigrator_Migrate(t *testing.T) { }, }) pr.On("SetMessage", mock.Anything, "export resources").Return() + pr.On("StrictMaxErrors", 1).Return() nc.On("Clean", mock.Anything, "test-namespace", pr).Return(errors.New("clean failed")) // Export and sync jobs succeed @@ -92,6 +95,7 @@ func TestUnifiedStorageMigrator_Migrate(t *testing.T) { }, }) pr.On("SetMessage", mock.Anything, "export resources").Return() + pr.On("StrictMaxErrors", 1).Return() // Export job succeeds ew.On("Process", mock.Anything, rw, mock.MatchedBy(func(job provisioning.Job) bool { return job.Spec.Push != nil diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker.go b/pkg/registry/apis/provisioning/jobs/migrate/worker.go index 736ecfc81b4..625b49ea32c 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/worker.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker.go @@ -43,7 +43,6 @@ func (w *MigrationWorker) Process(ctx context.Context, repo repository.Repositor return errors.New("missing migrate settings") } - progress.Strict() progress.SetTotal(ctx, 10) // will show a progress bar rw, ok := repo.(repository.ReaderWriter) if !ok { diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go b/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go index 31242a48189..e7d8c7c6175 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker_test.go @@ -62,7 +62,6 @@ func TestMigrationWorker_ProcessNotReaderWriter(t *testing.T) { } progressRecorder := jobs.NewMockJobProgressRecorder(t) progressRecorder.On("SetTotal", mock.Anything, 10).Return() - progressRecorder.On("Strict").Return() repo := repository.NewMockReader(t) err := worker.Process(context.Background(), repo, job, progressRecorder) @@ -87,7 +86,6 @@ func TestMigrationWorker_WithHistory(t *testing.T) { t.Run("fail local", func(t *testing.T) { progressRecorder := jobs.NewMockJobProgressRecorder(t) progressRecorder.On("SetTotal", mock.Anything, 10).Return() - progressRecorder.On("Strict").Return() repo := local.NewLocal(&provisioning.Repository{}, nil) err := worker.Process(context.Background(), repo, job, progressRecorder) @@ -97,7 +95,6 @@ func TestMigrationWorker_WithHistory(t *testing.T) { t.Run("fail unified", func(t *testing.T) { progressRecorder := jobs.NewMockJobProgressRecorder(t) progressRecorder.On("SetTotal", mock.Anything, 10).Return() - progressRecorder.On("Strict").Return() repo := repository.NewMockRepository(t) repo.On("Config").Return(&provisioning.Repository{ @@ -144,7 +141,6 @@ func TestMigrationWorker_Process(t *testing.T) { isLegacyActive: true, setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { pr.On("SetTotal", mock.Anything, 10).Return() - pr.On("Strict").Return() ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil) lm.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) }, @@ -160,7 +156,6 @@ func TestMigrationWorker_Process(t *testing.T) { isLegacyActive: false, setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { pr.On("SetTotal", mock.Anything, 10).Return() - pr.On("Strict").Return() ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(true, nil) um.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) }, @@ -176,7 +171,6 @@ func TestMigrationWorker_Process(t *testing.T) { isLegacyActive: true, setupMocks: func(lm *MockMigrator, um *MockMigrator, ds *dualwrite.MockService, pr *jobs.MockJobProgressRecorder) { pr.On("SetTotal", mock.Anything, 10).Return() - pr.On("Strict").Return() ds.On("ReadFromUnified", mock.Anything, mock.Anything).Return(false, nil) lm.On("Migrate", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("migration failed")) }, diff --git a/pkg/registry/apis/provisioning/jobs/progress.go b/pkg/registry/apis/provisioning/jobs/progress.go index d23abcd9cd2..081ac9a3b86 100644 --- a/pkg/registry/apis/provisioning/jobs/progress.go +++ b/pkg/registry/apis/provisioning/jobs/progress.go @@ -60,8 +60,7 @@ type jobProgressRecorder struct { func newJobProgressRecorder(ProgressFn ProgressFn) JobProgressRecorder { return &jobProgressRecorder{ - maxErrors: 20, - started: time.Now(), + started: time.Now(), // Have a faster notifier for messages and total notifyImmediatelyFn: maybeNotifyProgress(500*time.Millisecond, ProgressFn), maybeNotifyFn: maybeNotifyProgress(5*time.Second, ProgressFn), @@ -126,9 +125,9 @@ func (r *jobProgressRecorder) SetTotal(ctx context.Context, total int) { r.notifyImmediately(ctx) } -func (r *jobProgressRecorder) Strict() { +func (r *jobProgressRecorder) StrictMaxErrors(maxErrors int) { r.mu.Lock() - r.maxErrors = 1 + r.maxErrors = maxErrors r.mu.Unlock() } @@ -136,7 +135,7 @@ func (r *jobProgressRecorder) TooManyErrors() error { r.mu.RLock() defer r.mu.RUnlock() - if r.errorCount >= r.maxErrors { + if r.maxErrors > 0 && r.errorCount >= r.maxErrors { return fmt.Errorf("too many errors: %d", r.errorCount) } @@ -256,8 +255,13 @@ func (r *jobProgressRecorder) Complete(ctx context.Context, err error) provision // Check for errors during execution if len(jobStatus.Errors) > 0 && jobStatus.State != provisioning.JobStateError { - jobStatus.State = provisioning.JobStateError - jobStatus.Message = "completed with errors" + if r.TooManyErrors() != nil { + jobStatus.Message = "completed with too many errors" + jobStatus.State = provisioning.JobStateError + } else { + jobStatus.Message = "completed with errors" + jobStatus.State = provisioning.JobStateWarning + } } // Override message if progress have a more explicit message diff --git a/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go b/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go index cfe7e46f88b..fd634d60844 100644 --- a/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go +++ b/pkg/registry/apis/provisioning/jobs/progress_fn_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.52.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/queue.go b/pkg/registry/apis/provisioning/jobs/queue.go index 0502fb7886a..32b935e4be8 100644 --- a/pkg/registry/apis/provisioning/jobs/queue.go +++ b/pkg/registry/apis/provisioning/jobs/queue.go @@ -24,7 +24,7 @@ type JobProgressRecorder interface { SetMessage(ctx context.Context, msg string) SetTotal(ctx context.Context, total int) TooManyErrors() error - Strict() + StrictMaxErrors(maxErrors int) Complete(ctx context.Context, err error) provisioning.JobStatus } diff --git a/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go b/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go index b7dc38de650..d0152e725bc 100644 --- a/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go +++ b/pkg/registry/apis/provisioning/jobs/repo_getter_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.52.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker.go b/pkg/registry/apis/provisioning/jobs/sync/worker.go index 0fc3d839834..5b8eb701b34 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker.go @@ -97,6 +97,8 @@ func (r *SyncWorker) Process(ctx context.Context, repo repository.Repository, jo } progress.SetMessage(ctx, "execute sync job") + progress.StrictMaxErrors(20) // make it stop after 20 errors + currentRef, syncError := r.syncer.Sync(ctx, rw, *job.Spec.Pull, repositoryResources, clients, progress) jobStatus := progress.Complete(ctx, syncError) syncStatus = jobStatus.ToSyncStatus(job.Name) diff --git a/pkg/registry/apis/provisioning/jobs/sync/worker_test.go b/pkg/registry/apis/provisioning/jobs/sync/worker_test.go index b0171fc7fc3..02e152afb6a 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/worker_test.go @@ -225,6 +225,7 @@ func TestSyncWorker_Process(t *testing.T) { // Sync execution succeeds pr.On("SetMessage", mock.Anything, "execute sync job").Return() + pr.On("StrictMaxErrors", 20).Return() s.On("Sync", mock.Anything, rw, mock.MatchedBy(func(opts provisioning.SyncJobOptions) bool { return true // Add specific sync options validation if needed }), mockRepoResources, mock.Anything, pr).Return("new-ref", nil) @@ -277,6 +278,7 @@ func TestSyncWorker_Process(t *testing.T) { // Sync execution fails pr.On("SetMessage", mock.Anything, "execute sync job").Return() + pr.On("StrictMaxErrors", 20).Return() syncError := errors.New("sync operation failed") s.On("Sync", mock.Anything, rw, mock.MatchedBy(func(opts provisioning.SyncJobOptions) bool { return true // Add specific sync options validation if needed @@ -317,6 +319,7 @@ func TestSyncWorker_Process(t *testing.T) { mockClients := resources.NewMockResourceClients(t) cf.On("Clients", mock.Anything, mock.Anything).Return(mockClients, nil) pr.On("SetMessage", mock.Anything, mock.Anything).Return() + pr.On("StrictMaxErrors", 20).Return() pr.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{State: provisioning.JobStateSuccess}) rpf.On("Execute", mock.Anything, mock.Anything, mock.Anything).Return(nil) s.On("Sync", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("new-ref", nil) @@ -348,6 +351,7 @@ func TestSyncWorker_Process(t *testing.T) { mockClients := resources.NewMockResourceClients(t) cf.On("Clients", mock.Anything, mock.Anything).Return(mockClients, nil) pr.On("SetMessage", mock.Anything, mock.Anything).Return() + pr.On("StrictMaxErrors", 20).Return() pr.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{State: provisioning.JobStateSuccess}) s.On("Sync", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("new-ref", nil) }, @@ -407,6 +411,7 @@ func TestSyncWorker_Process(t *testing.T) { mockClients := resources.NewMockResourceClients(t) cf.On("Clients", mock.Anything, mock.Anything).Return(mockClients, nil) pr.On("SetMessage", mock.Anything, mock.Anything).Return() + pr.On("StrictMaxErrors", 20).Return() pr.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{State: provisioning.JobStateSuccess}) s.On("Sync", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("new-ref", nil) }, @@ -459,6 +464,7 @@ func TestSyncWorker_Process(t *testing.T) { mockClients := resources.NewMockResourceClients(t) cf.On("Clients", mock.Anything, mock.Anything).Return(mockClients, nil) pr.On("SetMessage", mock.Anything, mock.Anything).Return() + pr.On("StrictMaxErrors", 20).Return() pr.On("Complete", mock.Anything, mock.Anything).Return(provisioning.JobStatus{State: provisioning.JobStateSuccess}) s.On("Sync", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("new-ref", nil) }, @@ -489,6 +495,7 @@ func TestSyncWorker_Process(t *testing.T) { // Sync succeeds pr.On("SetMessage", mock.Anything, mock.Anything).Return() + pr.On("StrictMaxErrors", 20).Return() s.On("Sync", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("new-ref", nil) pr.On("Complete", mock.Anything, nil).Return(provisioning.JobStatus{State: provisioning.JobStateSuccess}) diff --git a/pkg/registry/apis/provisioning/jobs/worker_mock.go b/pkg/registry/apis/provisioning/jobs/worker_mock.go index 59e3c0df1b4..b264b258d1c 100644 --- a/pkg/registry/apis/provisioning/jobs/worker_mock.go +++ b/pkg/registry/apis/provisioning/jobs/worker_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.52.4. DO NOT EDIT. package jobs diff --git a/pkg/registry/apis/provisioning/resources/resources.go b/pkg/registry/apis/provisioning/resources/resources.go index b1f5cef53c2..28dd06ea43a 100644 --- a/pkg/registry/apis/provisioning/resources/resources.go +++ b/pkg/registry/apis/provisioning/resources/resources.go @@ -21,6 +21,7 @@ import ( var ( ErrAlreadyInRepository = errors.New("already in repository") + ErrDuplicateName = errors.New("duplicate name in repository") ErrMissingName = field.Required(field.NewPath("name", "metadata", "name"), "missing name in resource") ) @@ -152,7 +153,7 @@ func (r *ResourcesManager) WriteResourceFromFile(ctx context.Context, path strin } existing, found := r.resourcesLookup[id] if found { - return "", parsed.GVK, fmt.Errorf("duplicate resource name: %s, %s and %s", parsed.Obj.GetName(), path, existing) + return "", parsed.GVK, fmt.Errorf("duplicate resource name: %s, %s and %s: %w", parsed.Obj.GetName(), path, existing, ErrDuplicateName) } r.resourcesLookup[id] = path diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index 3fc3d7c5bb4..76fe0149441 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -3096,12 +3096,13 @@ "format": "int64" }, "state": { - "description": "Possible enum values:\n - `\"error\"` Finished with errors\n - `\"pending\"` Job has been submitted, but not processed yet\n - `\"success\"` Finished with success\n - `\"working\"` The job is running", + "description": "Possible enum values:\n - `\"error\"` Finished with errors\n - `\"pending\"` Job has been submitted, but not processed yet\n - `\"success\"` Finished with success\n - `\"warning\"` Finished with some non-critical errors\n - `\"working\"` The job is running", "type": "string", "enum": [ "error", "pending", "success", + "warning", "working" ] }, @@ -4048,13 +4049,14 @@ "format": "int64" }, "state": { - "description": "pending, running, success, error\n\nPossible enum values:\n - `\"error\"` Finished with errors\n - `\"pending\"` Job has been submitted, but not processed yet\n - `\"success\"` Finished with success\n - `\"working\"` The job is running", + "description": "pending, running, success, error\n\nPossible enum values:\n - `\"error\"` Finished with errors\n - `\"pending\"` Job has been submitted, but not processed yet\n - `\"success\"` Finished with success\n - `\"warning\"` Finished with some non-critical errors\n - `\"working\"` The job is running", "type": "string", "default": "", "enum": [ "error", "pending", "success", + "warning", "working" ] } diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts index 77a65889cca..7122a2e268b 100644 --- a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts @@ -819,8 +819,9 @@ export type JobStatus = { - `"error"` Finished with errors - `"pending"` Job has been submitted, but not processed yet - `"success"` Finished with success + - `"warning"` Finished with some non-critical errors - `"working"` The job is running */ - state?: 'error' | 'pending' | 'success' | 'working'; + state?: 'error' | 'pending' | 'success' | 'warning' | 'working'; /** Summary of processed actions */ summary?: JobResourceSummary[]; }; @@ -991,8 +992,9 @@ export type SyncStatus = { - `"error"` Finished with errors - `"pending"` Job has been submitted, but not processed yet - `"success"` Finished with success + - `"warning"` Finished with some non-critical errors - `"working"` The job is running */ - state: 'error' | 'pending' | 'success' | 'working'; + state: 'error' | 'pending' | 'success' | 'warning' | 'working'; }; export type WebhookStatus = { encryptedSecret?: string; diff --git a/public/app/features/provisioning/Job/FinishedJobStatus.tsx b/public/app/features/provisioning/Job/FinishedJobStatus.tsx index e51b79ebb5c..b5fd32c4353 100644 --- a/public/app/features/provisioning/Job/FinishedJobStatus.tsx +++ b/public/app/features/provisioning/Job/FinishedJobStatus.tsx @@ -42,6 +42,9 @@ export function FinishedJobStatus({ jobUid, repositoryName }: FinishedJobProps) }); } else if (job.status.state === 'success') { setStepStatusInfo({ status: 'success' }); + } else if (job.status.state === 'warning') { + // We treat warnings as success for now, but this could be changed later + setStepStatusInfo({ status: 'success' }); } } diff --git a/public/app/features/provisioning/Job/JobContent.tsx b/public/app/features/provisioning/Job/JobContent.tsx index 864da5adbc1..841800f2f7f 100644 --- a/public/app/features/provisioning/Job/JobContent.tsx +++ b/public/app/features/provisioning/Job/JobContent.tsx @@ -29,13 +29,22 @@ export function JobContent({ job, isFinishedJob = false }: JobContentProps) { title={t('provisioning.job-status.status.title-job-completed-successfully', 'Job completed successfully')} /> ); + case 'warning': + return ( + + {errors?.length ? errors?.join('\n') : message} + + ); case 'error': return ( - {message ?? errors?.join('\n')} + {errors?.length ? errors?.join('\n') : message} ); } diff --git a/public/app/features/provisioning/Job/RecentJobs.tsx b/public/app/features/provisioning/Job/RecentJobs.tsx index 8cf520979d3..cf1291bcb2f 100644 --- a/public/app/features/provisioning/Job/RecentJobs.tsx +++ b/public/app/features/provisioning/Job/RecentJobs.tsx @@ -26,8 +26,11 @@ const getStatusColor = (state?: SyncStatus['state']) => { case 'success': return 'green'; case 'working': - case 'pending': + return 'blue'; + case 'warning': return 'orange'; + case 'pending': + return 'darkgrey'; case 'error': return 'red'; default: diff --git a/public/app/features/provisioning/Shared/StatusBadge.tsx b/public/app/features/provisioning/Shared/StatusBadge.tsx index afd010c3b67..7923cf6549c 100644 --- a/public/app/features/provisioning/Shared/StatusBadge.tsx +++ b/public/app/features/provisioning/Shared/StatusBadge.tsx @@ -27,7 +27,7 @@ export function StatusBadge({ repo }: StatusBadgeProps) { text = 'Automatic pulling disabled'; icon = 'info-circle'; } else if (!repo.status?.sync?.state?.length) { - color = 'orange'; + color = 'darkgrey'; text = 'Pending'; icon = 'spinner'; tooltip = 'Waiting for health check to run'; @@ -40,8 +40,13 @@ export function StatusBadge({ repo }: StatusBadgeProps) { color = 'green'; break; case 'working': - case 'pending': + case 'warning': color = 'orange'; + text = 'warning'; + icon = 'exclamation-triangle'; + break; + case 'pending': + color = 'darkgrey'; text = 'Pulling'; icon = 'spinner'; break; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 42438db155f..7a1a46a7138 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11208,7 +11208,8 @@ "starting": "Starting...", "status": { "title-error-running-job": "Error running job", - "title-job-completed-successfully": "Job completed successfully" + "title-job-completed-successfully": "Job completed successfully", + "title-warning-running-job": "Job completed with warnings" }, "summary": "Summary" }, From 89c664dc05e9b0c448aad38c904f88fd18cf8069 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Wed, 23 Jul 2025 11:41:05 +0200 Subject: [PATCH 010/101] Chore/label internal issues 2 (#108479) * add PR author * see if this way of getting org members is correct * switch to env * add check for repository * cleanup logic a bit --- .github/workflows/issue-opened.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml index 76ebd06fe8b..3dc7cbad7f8 100644 --- a/.github/workflows/issue-opened.yml +++ b/.github/workflows/issue-opened.yml @@ -119,12 +119,34 @@ jobs: SLACK_WEBHOOK_URL: ${{ env.AUTOTRIAGER_SLACK_WEBHOOK_URL }} auto-label-internal-issues: needs: [main] + if: github.repository == 'grafana/grafana' runs-on: ubuntu-latest permissions: issues: write steps: + - name: "Get vault secrets" + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main # zizmor: ignore[unpinned-uses] + with: + # Secrets placed in the ci/repo/grafana/grafana/plugins_platform_issue_triager path in Vault + repo_secrets: | + GITHUB_APP_ID=plugins_platform_issue_triager_github_bot:app_id + GITHUB_APP_PRIVATE_KEY=plugins_platform_issue_triager_github_bot:app_pem + - name: Generate token + id: generate_token + uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 + with: + app-id: ${{ env.GITHUB_APP_ID }} + private-key: ${{ env.GITHUB_APP_PRIVATE_KEY }} + permission-members: read + - name: Check if member of grafana org + id: check-if-grafana-org-member + run: gh api https://api.github.com/orgs/grafana/members/${{ env.ACTOR }} >/dev/null 2>&1 && echo "is_grafana_org_member=true" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + ACTOR: ${{ github.actor }} - name: "Auto label internal issues" - if: github.repository == 'grafana/grafana' && (github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'OWNER') + if: steps.check-if-grafana-org-member.outputs.is_grafana_org_member == 'true' || github.event.issue.author_association == 'MEMBER' || github.event.issue.author_association == 'OWNER' run: gh issue edit "$NUMBER" --add-label "$LABELS" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 838391cd31230db72c2cf86e21f1390b4cb0ba39 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 23 Jul 2025 10:41:22 +0100 Subject: [PATCH 011/101] I18n: Prepare locale format for preview (#108501) * Promote toggle to public preview * disable RTL locales * update badge on prefs page to preview --- .../feature-toggles/index.md | 1 + pkg/services/featuremgmt/registry.go | 2 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.json | 9 +- .../SharedPreferences/SharedPreferences.tsx | 2 +- .../app/core/internationalization/locales.ts | 198 +++++++++++++++--- 6 files changed, 178 insertions(+), 36 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 5f3c0f3135e..290d3040229 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -107,6 +107,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `elasticsearchCrossClusterSearch` | Enables cross cluster search in the Elasticsearch datasource | | `alertRuleRestore` | Enables the alert rule restore feature | | `azureMonitorLogsBuilderEditor` | Enables the logs builder mode for the Azure Monitor data source | +| `localeFormatPreference` | Specifies the locale so the correct format for numbers and dates can be shown | | `logsPanelControls` | Enables a control component for the logs panel in Explore | ## Development feature toggles diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 35b8b06872d..3738ab4877a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1564,7 +1564,7 @@ var ( { Name: "localeFormatPreference", Description: "Specifies the locale so the correct format for numbers and dates can be shown", - Stage: FeatureStageExperimental, + Stage: FeatureStagePublicPreview, Owner: grafanaFrontendPlatformSquad, }, { diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 7c8dbf691f4..74b5f2a6fd9 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -203,7 +203,7 @@ alertingMigrationUI,GA,@grafana/alerting-squad,false,false,true alertingImportYAMLUI,GA,@grafana/alerting-squad,false,false,true unifiedStorageHistoryPruner,GA,@grafana/search-and-storage,false,false,false azureMonitorLogsBuilderEditor,preview,@grafana/partner-datasources,false,false,false -localeFormatPreference,experimental,@grafana/grafana-frontend-platform,false,false,false +localeFormatPreference,preview,@grafana/grafana-frontend-platform,false,false,false unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false extensionSidebar,experimental,@grafana/observability-logs,false,false,true alertingRulePermanentlyDelete,GA,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index f66d89f5c28..a7cc676d47a 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1854,12 +1854,15 @@ { "metadata": { "name": "localeFormatPreference", - "resourceVersion": "1750434297879", - "creationTimestamp": "2025-03-31T13:59:07Z" + "resourceVersion": "1753261256848", + "creationTimestamp": "2025-03-31T13:59:07Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-07-23 09:00:56.848352786 +0000 UTC" + } }, "spec": { "description": "Specifies the locale so the correct format for numbers and dates can be shown", - "stage": "experimental", + "stage": "preview", "codeowner": "@grafana/grafana-frontend-platform" } }, diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 2991942372b..7afc8df92a6 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -342,7 +342,7 @@ export class SharedPreferences extends PureComponent { Region format - + } description={t( diff --git a/public/app/core/internationalization/locales.ts b/public/app/core/internationalization/locales.ts index 9b811d39ba6..569bf3e7b5f 100644 --- a/public/app/core/internationalization/locales.ts +++ b/public/app/core/internationalization/locales.ts @@ -4,280 +4,418 @@ interface Locale { code: string; } -// TODO re-check translations +// Grafana does not support right-to-left languages in the UI, so to avoid confusion +// and a poor experience, we've commented out the right-to-left languages so they don't +// appear as options. export const LOCALES: Locale[] = [ // Afrikaans - Standard { name: 'Afrikaans', code: 'af' }, - // Arabic - Standard - { name: 'العربية', code: 'ar' }, - // Arabic - Algeria - { name: 'العربية (الجزائر)', code: 'ar-DZ' }, - // Arabic - Kuwait - { name: 'العربية (الكويت)', code: 'ar-KW' }, - // Arabic - Libya - { name: 'العربية (ليبيا)', code: 'ar-LY' }, - // Arabic - Morocco - { name: 'العربية (المغرب)', code: 'ar-MA' }, - // Arabic - Palestine - { name: 'العربية (فلسطين)', code: 'ar-PS' }, - // Arabic - Saudi Arabia - { name: 'العربية (السعودية)', code: 'ar-SA' }, - // Arabic - Tunisia - { name: 'العربية (تونس)', code: 'ar-TN' }, + + // // Arabic - Standard + // { name: 'العربية', code: 'ar' }, // Disabled because RTL + + // // Arabic - Algeria + // { name: 'العربية (الجزائر)', code: 'ar-DZ' }, // Disabled because RTL + + // // Arabic - Kuwait + // { name: 'العربية (الكويت)', code: 'ar-KW' }, // Disabled because RTL + + // // Arabic - Libya + // { name: 'العربية (ليبيا)', code: 'ar-LY' }, // Disabled because RTL + + // // Arabic - Morocco + // { name: 'العربية (المغرب)', code: 'ar-MA' }, // Disabled because RTL + + // // Arabic - Palestine + // { name: 'العربية (فلسطين)', code: 'ar-PS' }, // Disabled because RTL + + // // Arabic - Saudi Arabia + // { name: 'العربية (السعودية)', code: 'ar-SA' }, // Disabled because RTL + + // // Arabic - Tunisia + // { name: 'العربية (تونس)', code: 'ar-TN' }, // Disabled because RTL + // Azerbaijani - Azerbaijan { name: 'Azərbaycan dili', code: 'az' }, + // Belarusian - Belarus { name: 'Беларуская мова', code: 'be-BY' }, + // Bulgarian - Bulgaria { name: 'Български език', code: 'bg-BG' }, + // Bambara - Mali { name: 'Bamanankan', code: 'bm' }, + // Bengali - Standard { name: 'বাংলা', code: 'bn' }, + // Bengali - Bangladesh { name: 'বাংলা', code: 'bn-BD' }, + // Tibetan - Tibet (China) and Bhutan { name: 'བོད་ཡིག', code: 'bo' }, + // Breton - Brittany (France) { name: 'Brezhoneg', code: 'br' }, + // Bosnian - Bosnia and Herzegovina { name: 'Bosanski jezik', code: 'bs' }, + // Catalan - Catalonia (Spain) { name: 'Català', code: 'ca-ES' }, + // Czech - Czech Republic { name: 'Čeština', code: 'cs-CZ' }, + // Welsh - Wales (United Kingdom) { name: 'Cymraeg', code: 'cy-GB' }, + // Chuvash - Chuvashia (Russia) { name: 'Чӑвашла', code: 'cv-RU' }, + // Danish - Denmark { name: 'Dansk', code: 'da-DK' }, + // German - Germany { name: 'Deutsch', code: 'de-DE' }, + // German - Austria { name: 'Deutsch (Österreich)', code: 'de-AT' }, + // German - Switzerland { name: 'Deutsch (Schweiz)', code: 'de-CH' }, - // Divehi/Maldivian - Maldives - { name: 'ދިވެހި', code: 'dv-MV' }, + + // // Divehi/Maldivian - Maldives + // { name: 'ދިވެހި', code: 'dv-MV' }, // Disabled because RTL + // Greek - Greece { name: 'Ελληνικά', code: 'el-GR' }, + // English - Australia { name: 'English (Australia)', code: 'en-AU' }, + // English - Canada { name: 'English (Canada)', code: 'en-CA' }, + // English - United Kingdom { name: 'English (United Kingdom)', code: 'en-GB' }, + // English - Ireland { name: 'English (Ireland)', code: 'en-IE' }, + // English - Israel { name: 'English (Israel)', code: 'en-IL' }, + // English - India { name: 'English (India)', code: 'en-IN' }, + // English - New Zealand { name: 'English (New Zealand)', code: 'en-NZ' }, + // English - Singapore { name: 'English (Singapore)', code: 'en-SG' }, + // English - United States { name: 'English (United States)', code: 'en-US' }, + // Esperanto - International Auxiliary Language - { name: 'Esperanto', code: 'eo' }, + // { name: 'Esperanto', code: 'eo' }, + // Spanish - Spain { name: 'Español', code: 'es-ES' }, + // Spanish - Dominican Republic { name: 'Español (República Dominicana)', code: 'es-DO' }, + // Spanish - Mexico { name: 'Español (México)', code: 'es-MX' }, + // Spanish - United States { name: 'Español (Estados Unidos)', code: 'es-US' }, + // Estonian - Estonia { name: 'Eesti keel', code: 'et-EE' }, + // Basque - Basque Country { name: 'Euskara', code: 'eu-ES' }, - // Persian - Iran - { name: 'فارسی', code: 'fa-IR' }, + + // // Persian - Iran + // { name: 'فارسی', code: 'fa-IR' }, // Disabled because RTL + // Filipino - Philippines { name: 'Wikang Filipino', code: 'fil-PH' }, + // Finnish - Finland { name: 'Suomi', code: 'fi-FI' }, + // Faroese - Faroe Islands { name: 'Føroyskt', code: 'fo-FO' }, + // French - France { name: 'Français', code: 'fr-FR' }, + // French - Canada { name: 'Français (Canada)', code: 'fr-CA' }, + // French - Switzerland { name: 'Français (Suisse)', code: 'fr-CH' }, + // West Frisian - Netherlands { name: 'Frysk', code: 'fy' }, + // Irish - Ireland { name: 'Gaeilge', code: 'ga-IE' }, + // Scottish Gaelic - Scotland (UK) { name: 'Gàidhlig', code: 'gd-GB' }, + // Galician - Galicia (Spain) { name: 'Galego', code: 'gl-ES' }, + // Konkani (Devanagari script) - India { name: 'कोंकणी', code: 'gom-Deva' }, + // Konkani (Latin script) - India { name: 'Konkani', code: 'gom-Latn' }, + // Gujarati - India { name: 'ગુજરાતી', code: 'gu-IN' }, - // Hebrew - Israel - { name: 'עברית', code: 'he-IL' }, + + // // Hebrew - Israel + // { name: 'עברית', code: 'he-IL' }, // Disabled because RTL + // Hindi - India { name: 'हिन्दी', code: 'hi' }, + // Croatian - Croatia { name: 'Hrvatski jezik', code: 'hr' }, + // Hungarian - Hungary { name: 'Magyar nyelv', code: 'hu-HU' }, + // Armenian - Armenia { name: 'Հայերեն', code: 'hy-AM' }, + // Indonesian - Indonesia { name: 'Bahasa Indonesia', code: 'id-ID' }, + // Icelandic - Iceland { name: 'Íslenska', code: 'is-IS' }, + // Italian - Italy { name: 'Italiano', code: 'it-IT' }, + // Italian - Switzerland { name: 'Italiano (Svizzera)', code: 'it-CH' }, + // Japanese - Japan { name: '日本語', code: 'ja-JP' }, + // Javanese - Indonesia { name: 'ꦧꦱꦗꦮ', code: 'jv' }, + // Georgian - Georgia { name: 'ქართული ენა', code: 'ka-GE' }, + // Kazakh - Kazakhstan { name: 'Қазақ тілі', code: 'kk-KZ' }, + // Khmer - Cambodia { name: 'ខ្មែរ', code: 'km-KH' }, + // Kannada - India { name: 'ಕನ್ನಡ', code: 'kn-IN' }, + // Korean - South Korea { name: '한국어', code: 'ko-KR' }, + // Kurdish - Kurdistan (Iraq, Iran, Syria, Turkey) { name: 'Kurdî', code: 'ku' }, + // Kyrgyz - Kyrgyzstan { name: 'Кыргыз тили', code: 'ky-KG' }, + // Luxembourgish - Luxembourg { name: 'Lëtzebuergesch', code: 'lb-LU' }, + // Lao - Laos { name: 'ພາສາລາວ', code: 'lo-LA' }, + // Lithuanian - Lithuania { name: 'Lietuvių kalba', code: 'lt-LT' }, + // Latvian - Latvia { name: 'Latviešu valoda', code: 'lv-LV' }, + // Macedonian - North Macedonia { name: 'Македонски јазик', code: 'mk-MK' }, + // Malayalam - Kerala (India) { name: 'മലയാളം', code: 'ml-IN' }, + // Māori - New Zealand { name: 'Te Reo Māori', code: 'mi-NZ' }, + // Montenegrin - Montenegro { name: 'Црногорски језик', code: 'cnr-ME' }, + // Marathi - Maharashtra (India) { name: 'मराठी', code: 'mr' }, + // Malay - Malaysia, Singapore, Brunei { name: 'Bahasa Melayu', code: 'ms' }, + // Maltese - Malta { name: 'Malti', code: 'mt-MT' }, + // Mongolian - Mongolia { name: 'Монгол хэл', code: 'mn-MN' }, + // Burmese - Myanmar { name: 'မြန်မာစာ', code: 'my-MM' }, + // Norwegian Bokmål - Norway { name: 'Norsk bokmål', code: 'nb' }, + // Nepali - Nepal and India { name: 'नेपाली', code: 'ne' }, + // Dutch - Netherlands { name: 'Nederlands', code: 'nl-NL' }, + // Dutch - Belgium (Flemish) { name: 'Nederlands (België)', code: 'nl-BE' }, + // Norwegian Nynorsk - Norway { name: 'Nynorsk', code: 'nn-NO' }, + // Occitan - Southern France, Monaco, Italy { name: 'Occitan', code: 'oc' }, + // Punjabi - Punjab (India and Pakistan) { name: 'ਪੰਜਾਬੀ', code: 'pa' }, + // Polish - Poland { name: 'Polski', code: 'pl-PL' }, + // Portuguese - Portugal { name: 'Português', code: 'pt-PT' }, + // Portuguese - Brazil { name: 'Português (Brasil)', code: 'pt-BR' }, + // Romanian - Romania { name: 'Română', code: 'ro-RO' }, + // Russian - Russia { name: 'Русский язык', code: 'ru-RU' }, + // Northern Sami - Northern Scandinavia { name: 'Davvisámegiella', code: 'se' }, - // Sindhi - Pakistan and India - { name: 'سنڌي', code: 'sd' }, + + // // Sindhi - Pakistan and India + // { name: 'سنڌي', code: 'sd' }, // Disabled because RTL + // Sinhala - Sri Lanka { name: 'සිංහල', code: 'si-LK' }, + // Slovak - Slovakia { name: 'Slovenský jazyk', code: 'sk-SK' }, + // Slovenian - Slovenia { name: 'Slovenski jezik', code: 'sl-SI' }, + // Albanian - Albania, Kosovo { name: 'Shqip', code: 'sq' }, + // Serbian - Serbia (Default) { name: 'Српски', code: 'sr' }, + // Serbian - Serbia (Cyrillic script) { name: 'Српски (ћирилица)', code: 'sr-Cyrl' }, + // Swati - Eswatini (Swaziland) { name: 'SiSwati', code: 'ss' }, + // Swahili - East Africa { name: 'Kiswahili', code: 'sw' }, + // Swedish - Sweden { name: 'Svenska', code: 'sv' }, + // Tamil - Tamil Nadu (India), Sri Lanka, Singapore { name: 'தமிழ்', code: 'ta' }, + // Telugu - Andhra Pradesh, Telangana (India) { name: 'తెలుగు', code: 'te' }, + // Tetum - East Timor { name: 'Tetun', code: 'tet' }, + // Tajik - Tajikistan { name: 'Тоҷикӣ', code: 'tg' }, + // Thai - Thailand { name: 'ภาษาไทย', code: 'th-TH' }, + // Turkmen - Turkmenistan { name: 'Türkmen dili', code: 'tk-TM' }, + // Tagalog - Philippines { name: 'Wikang Tagalog', code: 'tl-PH' }, + // Klingon - Constructed Language (Star Trek) { name: 'tlhIngan Hol', code: 'tlh' }, + // Turkish - Turkey { name: 'Türkçe', code: 'tr-TR' }, + // Talossan - Constructed Language { name: 'Talossan', code: 'tzl' }, + // Tamazight (Tifinagh script) - North Africa { name: 'ⵜⴰⵎⴰⵣⵉⵖⵜ', code: 'tzm' }, + // Tamazight (Latin script) - North Africa { name: 'Tamazight', code: 'tzm-Latn' }, - // Uyghur - Xinjiang (China) - { name: 'ئۇيغۇرچە', code: 'ug-CN' }, + + // // Uyghur - Xinjiang (China) + // { name: 'ئۇيغۇرچە', code: 'ug-CN' }, // Disabled because RTL + // Ukrainian - Ukraine { name: 'Українська мова', code: 'uk-UA' }, - // Urdu - Pakistan and India - { name: 'اردو', code: 'ur-PK' }, + + // // Urdu - Pakistan and India + // { name: 'اردو', code: 'ur-PK' }, // Disabled because RTL + // Uzbek - Uzbekistan (Cyrillic script) { name: 'Ўзбек тили', code: 'uz-UZ' }, + // Uzbek - Uzbekistan (Latin script) { name: "O'zbek tili", code: 'uz-Latn' }, + // Vietnamese - Vietnam { name: 'Tiếng Việt', code: 'vi-VN' }, + // Chinese - China { name: '中文', code: 'zh-CN' }, + // Chinese - Simplified { name: '简体中文', code: 'zh-Hans' }, + // Chinese - Traditional { name: '繁體中文', code: 'zh-Hant' }, + // Chinese - Hong Kong { name: '中文 (香港)', code: 'zh-HK' }, + // Chinese - Taiwan { name: '正體中文 (台灣)', code: 'zh-TW' }, + // Chinese - Macau { name: '中文 (澳門)', code: 'zh-MO' }, + // Yoruba - Nigeria, Benin, Togo { name: 'Yorùbá', code: 'yo' }, ]; From f657044afb5039c7348cd520a6411a548cda1166 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Wed, 23 Jul 2025 12:12:47 +0200 Subject: [PATCH 012/101] LogLine: Add `Explain in Assistant` option to explain single log lines (#108387) * LogLine: Add `Explain in Assistant` option * LogLine: Improve menu and prompt * LogLine: Mock `@grafana/assistant` in tests * LogLine: Mock `@grafana/assistant` in tests * i18n * LogLine: Expose via loglistcontext * LogLine: Mock in LogLineDetails * revert * revert changes * LogLine: Fix LogListContext mock * LogLineControls: Add context props * LogLineControls: Mock `@grafana/assistant` * LogLineControls: Remove from props * LogLineControls: Lint menu * LogsPanel: Mock `@grafana/assistant` --- package.json | 1 + .../logs/components/panel/LogLine.test.tsx | 5 ++ .../components/panel/LogLineDetails.test.tsx | 7 +++ .../components/panel/LogLineDetailsHeader.tsx | 12 +++++ .../components/panel/LogLineMenu.test.tsx | 10 ++++ .../logs/components/panel/LogLineMenu.tsx | 13 ++++- .../logs/components/panel/LogList.test.tsx | 5 ++ .../logs/components/panel/LogListContext.tsx | 53 ++++++++++++++++++- .../components/panel/LogListControls.test.tsx | 9 ++++ .../panel/__mocks__/LogListContext.tsx | 11 ++++ .../plugins/panel/logs-new/LogsPanel.test.tsx | 5 ++ .../app/plugins/panel/logs/LogsPanel.test.tsx | 7 +++ public/locales/en-US/grafana.json | 4 ++ yarn.lock | 15 ++++++ 14 files changed, 154 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index dd6ba11bc16..54a1d756b6c 100644 --- a/package.json +++ b/package.json @@ -271,6 +271,7 @@ "@formatjs/intl-durationformat": "^0.7.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/alerting": "workspace:*", + "@grafana/assistant": "0.0.7", "@grafana/aws-sdk": "0.7.1", "@grafana/azure-sdk": "0.0.7", "@grafana/data": "workspace:*", diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx index e31ba38cabf..89803a457bb 100644 --- a/public/app/features/logs/components/panel/LogLine.test.tsx +++ b/public/app/features/logs/components/panel/LogLine.test.tsx @@ -14,6 +14,11 @@ import { defaultProps, defaultValue } from './__mocks__/LogListContext'; import { LogListModel } from './processing'; import { LogLineVirtualization } from './virtualization'; +jest.mock('@grafana/assistant', () => ({ + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn(() => [true, jest.fn()]), +})); + jest.mock('./LogListContext'); jest.mock('../LogDetails'); diff --git a/public/app/features/logs/components/panel/LogLineDetails.test.tsx b/public/app/features/logs/components/panel/LogLineDetails.test.tsx index 2ce960fdd86..112ea2531ba 100644 --- a/public/app/features/logs/components/panel/LogLineDetails.test.tsx +++ b/public/app/features/logs/components/panel/LogLineDetails.test.tsx @@ -23,6 +23,13 @@ import { LogLineDetails, Props } from './LogLineDetails'; import { LogListContext, LogListContextData } from './LogListContext'; import { defaultValue } from './__mocks__/LogListContext'; +jest.mock('@grafana/assistant', () => { + return { + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn().mockReturnValue([true, jest.fn()]), + }; +}); + jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), diff --git a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx index a71b64fc31b..9a4e6eedce0 100644 --- a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx +++ b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx @@ -37,6 +37,8 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr onPinLine, onUnpinLine, wrapLogMessage, + isAssistantAvailable, + openAssistantByLog, } = useLogListContext(); const pinned = useLogIsPinned(log); const styles = useStyles2(getStyles, detailsMode, wrapLogMessage); @@ -149,6 +151,16 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr suffix={search !== '' ? clearSearch : undefined} />
+ {isAssistantAvailable && ( + openAssistantByLog?.(log)} + tabIndex={0} + /> + )} {focusLogLine && ( ({ + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn(() => [true, jest.fn()]), +})); + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + isAssistantAvailable: true, +})); + const theme = createTheme(); const styles = getStyles(theme); const contextProps = { diff --git a/public/app/features/logs/components/panel/LogLineMenu.tsx b/public/app/features/logs/components/panel/LogLineMenu.tsx index 37a0df589d6..49b55b368d1 100644 --- a/public/app/features/logs/components/panel/LogLineMenu.tsx +++ b/public/app/features/logs/components/panel/LogLineMenu.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, MouseEvent } from 'react'; +import { MouseEvent, useCallback, useMemo, useRef } from 'react'; import { LogRowContextOptions, LogRowModel } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -45,6 +45,8 @@ export const LogLineMenu = ({ log, styles }: Props) => { logLineMenuCustomItems = [], logSupportsContext, toggleDetails, + isAssistantAvailable, + openAssistantByLog, } = useLogListContext(); const pinned = useLogIsPinned(log); const menuRef = useRef(null); @@ -108,6 +110,13 @@ export const LogLineMenu = ({ log, styles }: Props) => { {onPermalinkClick && log.rowId !== undefined && log.uid && ( )} + {isAssistantAvailable && ( + openAssistantByLog?.(log)} + icon="ai-sparkle" + label={t('logs.log-line-menu.open-assistant', 'Explain this log line in Assistant')} + /> + )} {logLineMenuCustomItems.map((item, i) => { if (isDivider(item)) { return ; @@ -134,6 +143,8 @@ export const LogLineMenu = ({ log, styles }: Props) => { showContext, toggleLogDetails, togglePinning, + isAssistantAvailable, + openAssistantByLog, ] ); diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx index 3d76e63e413..4f740d8059b 100644 --- a/public/app/features/logs/components/panel/LogList.test.tsx +++ b/public/app/features/logs/components/panel/LogList.test.tsx @@ -9,6 +9,11 @@ import { createLogRow } from '../mocks/logRow'; import { LogList, Props } from './LogList'; +jest.mock('@grafana/assistant', () => ({ + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn(() => [true, jest.fn()]), +})); + jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index 153c53a0a80..f2c7455fddb 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -11,6 +11,7 @@ import { useState, } from 'react'; +import { createContext as createAssistantContext, ItemDataType, useAssistant } from '@grafana/assistant'; import { CoreApp, DataFrame, @@ -22,10 +23,11 @@ import { shallowCompare, store, } from '@grafana/data'; -import { config, reportInteraction } from '@grafana/runtime'; +import { t } from '@grafana/i18n'; +import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; import { PopoverContent } from '@grafana/ui'; -import { DownloadFormat, checkLogsError, checkLogsSampled, downloadLogs as download } from '../../utils'; +import { checkLogsError, checkLogsSampled, downloadLogs as download, DownloadFormat } from '../../utils'; import { getDisplayedFieldsForLogs } from '../otel/formats'; import { LogLineDetailsMode } from './LogLineDetails'; @@ -63,6 +65,8 @@ export interface LogListContextData extends Omit void; showDetails: LogListModel[]; toggleDetails: (log: LogListModel) => void; + isAssistantAvailable: boolean; + openAssistantByLog: ((log: LogListModel) => void) | undefined; } export const LogListContext = createContext({ @@ -100,6 +104,8 @@ export const LogListContext = createContext({ syntaxHighlighting: true, toggleDetails: () => {}, wrapLogMessage: false, + isAssistantAvailable: false, + openAssistantByLog: () => {}, }); export const useLogListContextData = (key: keyof LogListContextData) => { @@ -239,6 +245,47 @@ export const LogListContextProvider = ({ const [showDetails, setShowDetails] = useState([]); const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey)); const [detailsMode, setDetailsMode] = useState(detailsModeProp ?? 'sidebar'); + const [isAssistantAvailable, openAssistant] = useAssistant(); + + const openAssistantByLog = useCallback( + async (log: LogListModel) => { + if (!openAssistant) { + return; + } + + const datasource = await getDataSourceSrv().get(log.datasourceUid); + const context = []; + if (datasource) { + context.push( + createAssistantContext(ItemDataType.Datasource, { + datasourceUid: datasource.uid, + datasourceName: datasource.name, + datasourceType: datasource.type, + }) + ); + } + openAssistant({ + prompt: `${t('logs.log-line-menu.log-line-explainer', 'Explain this log line in a concise way')}: + + \`\`\` +${log.entry.replaceAll('`', '\\`')} + \`\`\` + `, + context: [ + ...context, + createAssistantContext(ItemDataType.Structured, { + title: t('logs.log-line-menu.log-line', 'Log line'), + data: { + labels: log.labels, + value: log.entry, + timestamp: log.timestamp, + }, + }), + ], + }); + }, + [openAssistant] + ); useEffect(() => { if (noInteractions) { @@ -569,6 +616,8 @@ export const LogListContextProvider = ({ syntaxHighlighting: logListState.syntaxHighlighting, toggleDetails, wrapLogMessage: logListState.wrapLogMessage, + isAssistantAvailable, + openAssistantByLog, }} > {children} diff --git a/public/app/features/logs/components/panel/LogListControls.test.tsx b/public/app/features/logs/components/panel/LogListControls.test.tsx index db8e59134b0..2acdf38abe0 100644 --- a/public/app/features/logs/components/panel/LogListControls.test.tsx +++ b/public/app/features/logs/components/panel/LogListControls.test.tsx @@ -17,6 +17,13 @@ jest.mock('../../utils', () => ({ downloadLogs: jest.fn(), })); +jest.mock('@grafana/assistant', () => { + return { + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn().mockReturnValue([true, jest.fn()]), + }; +}); + const fontSize: LogListFontSize = 'default'; const contextProps = { app: CoreApp.Unknown, @@ -31,6 +38,8 @@ const contextProps = { sortOrder: LogsSortOrder.Ascending, syntaxHighlighting: false, wrapLogMessage: false, + isAssistantAvailable: false, + openAssistantByLog: () => {}, }; describe('LogListControls', () => { diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx index 131afaa32cb..53b020bbe88 100644 --- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx +++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx @@ -7,6 +7,13 @@ import { LogLineDetailsMode } from '../LogLineDetails'; import { LogListContextData, Props } from '../LogListContext'; import { LogListModel } from '../processing'; +jest.mock('@grafana/assistant', () => { + return { + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn().mockReturnValue([true, jest.fn()]), + }; +}); + export const LogListContext = createContext({ app: CoreApp.Unknown, closeDetails: () => {}, @@ -43,6 +50,8 @@ export const LogListContext = createContext({ setDetailsMode: function (mode: LogLineDetailsMode): void { throw new Error('Function not implemented.'); }, + isAssistantAvailable: false, + openAssistantByLog: () => {}, }); export const useLogListContextData = (key: keyof LogListContextData) => { @@ -97,6 +106,8 @@ export const defaultValue: LogListContextData = { showTime: false, sortOrder: LogsSortOrder.Ascending, wrapLogMessage: false, + isAssistantAvailable: false, + openAssistantByLog: () => {}, }; export const defaultProps: Props = { diff --git a/public/app/plugins/panel/logs-new/LogsPanel.test.tsx b/public/app/plugins/panel/logs-new/LogsPanel.test.tsx index 77570bf19ab..09997a66047 100644 --- a/public/app/plugins/panel/logs-new/LogsPanel.test.tsx +++ b/public/app/plugins/panel/logs-new/LogsPanel.test.tsx @@ -18,6 +18,11 @@ import { LogsPanel } from './LogsPanel'; type LogsPanelProps = ComponentProps; +jest.mock('@grafana/assistant', () => ({ + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn(() => [true, jest.fn()]), +})); + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getAppEvents: jest.fn(), diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx index 6a299326f85..64ef4a60a44 100644 --- a/public/app/plugins/panel/logs/LogsPanel.test.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx @@ -57,6 +57,13 @@ jest.mock('@grafana/data', () => ({ hasLogsContextSupport: (ds: MockDataSourceApi) => hasLogsContextSupport(ds), })); +jest.mock('@grafana/assistant', () => { + return { + ...jest.requireActual('@grafana/assistant'), + useAssistant: jest.fn().mockReturnValue([true, jest.fn()]), + }; +}); + const defaultProps = { data: { error: undefined, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7a1a46a7138..180039f2b38 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -9331,6 +9331,7 @@ "move-displayed-field-down": "Move down", "move-displayed-field-up": "Move up", "no-details": "No fields to display.", + "open-assistant": "Explain this log line in Assistant", "pin-line": "Pin log", "remove-displayed-field": "Remove field", "remove-log": "Remove log", @@ -9349,6 +9350,9 @@ "copy-log": "Copy log line", "hide-details": "Show log details", "icon-label": "Log menu", + "log-line": "Log line", + "log-line-explainer": "Explain this log line in a concise way", + "open-assistant": "Explain this log line in Assistant", "pin-to-outline": "Pin log", "show-context": "Show context", "show-details": "Hide log details", diff --git a/yarn.lock b/yarn.lock index 866a76ae882..7bcd86a7b5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3064,6 +3064,20 @@ __metadata: languageName: unknown linkType: soft +"@grafana/assistant@npm:0.0.7": + version: 0.0.7 + resolution: "@grafana/assistant@npm:0.0.7" + peerDependencies: + "@grafana/data": ">=12.0.0" + "@grafana/runtime": ">=12.0.0" + "@grafana/scenes": ">=5.41.0" + "@grafana/ui": ">=12.0.0" + react: ">=18.0.0" + rxjs: ">=7.0.0" + checksum: 10/c83523a032cb748867f683e87ad5b00a04bb4918ae5c7aafe5587b418b73b769a92de948335f0d9055d06036004e7a9794722f627cab3111b076eb27423d4344 + languageName: node + linkType: hard + "@grafana/async-query-data@npm:0.4.1": version: 0.4.1 resolution: "@grafana/async-query-data@npm:0.4.1" @@ -17832,6 +17846,7 @@ __metadata: "@formatjs/intl-durationformat": "npm:^0.7.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/alerting": "workspace:*" + "@grafana/assistant": "npm:0.0.7" "@grafana/aws-sdk": "npm:0.7.1" "@grafana/azure-sdk": "npm:0.0.7" "@grafana/data": "workspace:*" From 0e7b041b27e46b00be6854de767a8826f5daa91a Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Wed, 23 Jul 2025 12:14:28 +0200 Subject: [PATCH 013/101] Secrets: Move decrypt types to contracts and export public at root pkg (#108376) * Secrets: Move decrypt types to contracts and export public at root pkg * Provisioning: Replace decrypt pkg imports * Merge wire changes --- .../secrets/mocks/decrypt_service_mock.go | 16 ++++----- .../apis/provisioning/secrets/repository.go | 8 ++--- .../apis/provisioning/secrets/secret.go | 13 +++---- .../apis/provisioning/secrets/secret_test.go | 18 +++++----- pkg/registry/apis/secret/contracts/decrypt.go | 29 +++++++++++++++ pkg/registry/apis/secret/decrypt/service.go | 13 +++---- .../apis/secret/decrypt/service_test.go | 18 +++++----- pkg/registry/apis/secret/decrypt_client.go | 25 +++++++++++++ pkg/registry/apis/secret/errors.go | 8 ----- .../apis/secret/secure_value_client.go | 5 +++ pkg/registry/apis/secret/service/decrypt.go | 36 ------------------- .../apis/secret/testutils/testutils.go | 2 +- pkg/server/wire_gen.go | 16 ++++----- .../secret/metadata/secure_value_test.go | 15 ++++---- 14 files changed, 115 insertions(+), 107 deletions(-) create mode 100644 pkg/registry/apis/secret/decrypt_client.go delete mode 100644 pkg/registry/apis/secret/errors.go delete mode 100644 pkg/registry/apis/secret/service/decrypt.go diff --git a/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go b/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go index 50ad9773e93..14e2d028689 100644 --- a/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go +++ b/pkg/registry/apis/provisioning/secrets/mocks/decrypt_service_mock.go @@ -5,7 +5,7 @@ package mocks import ( context "context" - service "github.com/grafana/grafana/pkg/registry/apis/secret/service" + contracts "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" mock "github.com/stretchr/testify/mock" ) @@ -23,7 +23,7 @@ func (_m *MockDecryptService) EXPECT() *MockDecryptService_Expecter { } // Decrypt provides a mock function with given fields: ctx, namespace, names -func (_m *MockDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]service.DecryptResult, error) { +func (_m *MockDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]contracts.DecryptResult, error) { _va := make([]interface{}, len(names)) for _i := range names { _va[_i] = names[_i] @@ -37,16 +37,16 @@ func (_m *MockDecryptService) Decrypt(ctx context.Context, namespace string, nam panic("no return value specified for Decrypt") } - var r0 map[string]service.DecryptResult + var r0 map[string]contracts.DecryptResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, ...string) (map[string]service.DecryptResult, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) (map[string]contracts.DecryptResult, error)); ok { return rf(ctx, namespace, names...) } - if rf, ok := ret.Get(0).(func(context.Context, string, ...string) map[string]service.DecryptResult); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, ...string) map[string]contracts.DecryptResult); ok { r0 = rf(ctx, namespace, names...) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(map[string]service.DecryptResult) + r0 = ret.Get(0).(map[string]contracts.DecryptResult) } } @@ -86,12 +86,12 @@ func (_c *MockDecryptService_Decrypt_Call) Run(run func(ctx context.Context, nam return _c } -func (_c *MockDecryptService_Decrypt_Call) Return(_a0 map[string]service.DecryptResult, _a1 error) *MockDecryptService_Decrypt_Call { +func (_c *MockDecryptService_Decrypt_Call) Return(_a0 map[string]contracts.DecryptResult, _a1 error) *MockDecryptService_Decrypt_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockDecryptService_Decrypt_Call) RunAndReturn(run func(context.Context, string, ...string) (map[string]service.DecryptResult, error)) *MockDecryptService_Decrypt_Call { +func (_c *MockDecryptService_Decrypt_Call) RunAndReturn(run func(context.Context, string, ...string) (map[string]contracts.DecryptResult, error)) *MockDecryptService_Decrypt_Call { _c.Call.Return(run) return _c } diff --git a/pkg/registry/apis/provisioning/secrets/repository.go b/pkg/registry/apis/provisioning/secrets/repository.go index c86917cb452..c933d72bf92 100644 --- a/pkg/registry/apis/provisioning/secrets/repository.go +++ b/pkg/registry/apis/provisioning/secrets/repository.go @@ -7,17 +7,17 @@ import ( "github.com/grafana/grafana-app-sdk/logging" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/grafana/grafana/pkg/services/featuremgmt" - grafanasecrets "github.com/grafana/grafana/pkg/services/secrets" + legacysecrets "github.com/grafana/grafana/pkg/services/secrets" ) func ProvideRepositorySecrets( features featuremgmt.FeatureToggles, - legacySecretsSvc grafanasecrets.Service, + legacySecretsSvc legacysecrets.Service, secretsSvc contracts.SecureValueClient, - decryptSvc service.DecryptService, + decryptSvc secret.DecryptService, ) RepositorySecrets { return NewRepositorySecrets(features, NewSecretsService(secretsSvc, decryptSvc), NewSingleTenant(legacySecretsSvc)) } diff --git a/pkg/registry/apis/provisioning/secrets/secret.go b/pkg/registry/apis/provisioning/secrets/secret.go index 8353499adb6..de3e43deb63 100644 --- a/pkg/registry/apis/provisioning/secrets/secret.go +++ b/pkg/registry/apis/provisioning/secrets/secret.go @@ -6,20 +6,17 @@ import ( "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - grafanasecrets "github.com/grafana/grafana/pkg/registry/apis/secret/service" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/client-go/dynamic" ) const svcName = "provisioning" //go:generate mockery --name SecureValueClient --structname MockSecureValueClient --inpackage --filename secure_value_client_mock.go --with-expecter -type SecureValueClient interface { - Client(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) -} +type SecureValueClient = secret.SecureValueClient //go:generate mockery --name Service --structname MockService --inpackage --filename secret_mock.go --with-expecter type Service interface { @@ -30,13 +27,13 @@ type Service interface { var _ Service = (*secretsService)(nil) -//go:generate mockery --name DecryptService --structname MockDecryptService --srcpkg=github.com/grafana/grafana/pkg/registry/apis/secret/service --filename decrypt_service_mock.go --with-expecter +//go:generate mockery --name DecryptService --structname MockDecryptService --srcpkg=github.com/grafana/grafana/pkg/registry/apis/secret --filename decrypt_service_mock.go --with-expecter type secretsService struct { secureValues SecureValueClient - decryptSvc grafanasecrets.DecryptService + decryptSvc secret.DecryptService } -func NewSecretsService(secretsSvc SecureValueClient, decryptSvc grafanasecrets.DecryptService) Service { +func NewSecretsService(secretsSvc SecureValueClient, decryptSvc secret.DecryptService) Service { return &secretsService{ secureValues: secretsSvc, decryptSvc: decryptSvc, diff --git a/pkg/registry/apis/provisioning/secrets/secret_test.go b/pkg/registry/apis/provisioning/secrets/secret_test.go index f7b12508b91..25b71bef1ad 100644 --- a/pkg/registry/apis/provisioning/secrets/secret_test.go +++ b/pkg/registry/apis/provisioning/secrets/secret_test.go @@ -7,8 +7,8 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets/mocks" + "github.com/grafana/grafana/pkg/registry/apis/secret" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -258,7 +258,7 @@ func TestSecretsService_Decrypt(t *testing.T) { secretName: "test-secret", setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *mocks.MockDecryptService) { exposedValue := secretv1beta1.NewExposedSecureValue("decrypted-data") - mockResult := service.NewDecryptResultValue(&exposedValue) + mockResult := secret.NewDecryptResultValue(&exposedValue) mockDecryptSvc.EXPECT().Decrypt( mock.MatchedBy(func(ctx context.Context) bool { @@ -267,7 +267,7 @@ func TestSecretsService_Decrypt(t *testing.T) { }), "test-namespace", "test-secret", - ).Return(map[string]service.DecryptResult{ + ).Return(map[string]secret.DecryptResult{ "test-secret": mockResult, }, nil) }, @@ -299,16 +299,16 @@ func TestSecretsService_Decrypt(t *testing.T) { }), "test-namespace", "test-secret", - ).Return(map[string]service.DecryptResult{}, nil) + ).Return(map[string]secret.DecryptResult{}, nil) }, - expectedError: contracts.ErrDecryptNotFound.Error(), + expectedError: secret.ErrDecryptNotFound.Error(), }, { name: "decrypt result has error", namespace: "test-namespace", secretName: "test-secret", setupMocks: func(mockSecretsSvc *MockSecureValueClient, mockDecryptSvc *mocks.MockDecryptService) { - mockResult := service.NewDecryptResultErr(errors.New("decryption failed")) + mockResult := secret.NewDecryptResultErr(errors.New("decryption failed")) mockDecryptSvc.EXPECT().Decrypt( mock.MatchedBy(func(ctx context.Context) bool { @@ -316,7 +316,7 @@ func TestSecretsService_Decrypt(t *testing.T) { }), "test-namespace", "test-secret", - ).Return(map[string]service.DecryptResult{ + ).Return(map[string]secret.DecryptResult{ "test-secret": mockResult, }, nil) }, @@ -354,7 +354,7 @@ func TestSecretsService_Decrypt_ServiceIdentityContext(t *testing.T) { mockDecryptSvc := &mocks.MockDecryptService{} exposedValue := secretv1beta1.NewExposedSecureValue("test-data") - mockResult := service.NewDecryptResultValue(&exposedValue) + mockResult := secret.NewDecryptResultValue(&exposedValue) // Create a more detailed context matcher to verify the service identity context is created correctly mockDecryptSvc.EXPECT().Decrypt( @@ -364,7 +364,7 @@ func TestSecretsService_Decrypt_ServiceIdentityContext(t *testing.T) { }), "test-namespace", "test-secret", - ).Return(map[string]service.DecryptResult{ + ).Return(map[string]secret.DecryptResult{ "test-secret": mockResult, }, nil) diff --git a/pkg/registry/apis/secret/contracts/decrypt.go b/pkg/registry/apis/secret/contracts/decrypt.go index 13c28b1025c..9c0d8e8eb2b 100644 --- a/pkg/registry/apis/secret/contracts/decrypt.go +++ b/pkg/registry/apis/secret/contracts/decrypt.go @@ -23,3 +23,32 @@ type DecryptStorage interface { type DecryptAuthorizer interface { Authorize(ctx context.Context, secureValueName string, secureValueDecrypters []string) (identity string, allowed bool) } + +// DecryptService is the inferface for the decrypt service. +type DecryptService interface { + Decrypt(ctx context.Context, namespace string, names ...string) (map[string]DecryptResult, error) +} + +// DecryptResult is the (union) result of a decryption operation. +// It contains the decrypted `value` when the decryption succeeds, and the `err` when it fails. +// It is not possible to construct a `DecryptResult` where both `value` and `err` are set from another package. +type DecryptResult struct { + value *secretv1beta1.ExposedSecureValue + err error +} + +func (d DecryptResult) Error() error { + return d.err +} + +func (d DecryptResult) Value() *secretv1beta1.ExposedSecureValue { + return d.value +} + +func NewDecryptResultErr(err error) DecryptResult { + return DecryptResult{err: err} +} + +func NewDecryptResultValue(value *secretv1beta1.ExposedSecureValue) DecryptResult { + return DecryptResult{value: value} +} diff --git a/pkg/registry/apis/secret/decrypt/service.go b/pkg/registry/apis/secret/decrypt/service.go index d35fb6eb2e9..38556c0c4f9 100644 --- a/pkg/registry/apis/secret/decrypt/service.go +++ b/pkg/registry/apis/secret/decrypt/service.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" ) @@ -12,23 +11,21 @@ type OSSDecryptService struct { decryptStore contracts.DecryptStorage } -var _ service.DecryptService = &OSSDecryptService{} - -func ProvideDecryptService(decryptStore contracts.DecryptStorage) service.DecryptService { +func ProvideDecryptService(decryptStore contracts.DecryptStorage) contracts.DecryptService { return &OSSDecryptService{ decryptStore: decryptStore, } } -func (d *OSSDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]service.DecryptResult, error) { - results := make(map[string]service.DecryptResult, len(names)) +func (d *OSSDecryptService) Decrypt(ctx context.Context, namespace string, names ...string) (map[string]contracts.DecryptResult, error) { + results := make(map[string]contracts.DecryptResult, len(names)) for _, name := range names { exposedSecureValue, err := d.decryptStore.Decrypt(ctx, xkube.Namespace(namespace), name) if err != nil { - results[name] = service.NewDecryptResultErr(err) + results[name] = contracts.NewDecryptResultErr(err) } else { - results[name] = service.NewDecryptResultValue(&exposedSecureValue) + results[name] = contracts.NewDecryptResultValue(&exposedSecureValue) } } diff --git a/pkg/registry/apis/secret/decrypt/service_test.go b/pkg/registry/apis/secret/decrypt/service_test.go index 0aaac869a67..3203e3ebbe7 100644 --- a/pkg/registry/apis/secret/decrypt/service_test.go +++ b/pkg/registry/apis/secret/decrypt/service_test.go @@ -6,7 +6,7 @@ import ( "testing" secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" - "github.com/grafana/grafana/pkg/registry/apis/secret/service" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -23,8 +23,8 @@ func TestDecryptService(t *testing.T) { mockErr := errors.New("mock error") mockStorage := &MockDecryptStorage{} mockStorage.On("Decrypt", mock.Anything, mock.Anything, mock.Anything).Return(secretv1beta1.ExposedSecureValue(""), mockErr) - decryptedValuesResp := map[string]service.DecryptResult{ - "secure-value-1": service.NewDecryptResultErr(mockErr), + decryptedValuesResp := map[string]contracts.DecryptResult{ + "secure-value-1": contracts.NewDecryptResultErr(mockErr), } decryptService := &OSSDecryptService{ @@ -49,9 +49,9 @@ func TestDecryptService(t *testing.T) { mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-2"). Return(exposedSecureValue2, nil) - decryptedValuesResp := map[string]service.DecryptResult{ - "secure-value-1": service.NewDecryptResultValue(&exposedSecureValue1), - "secure-value-2": service.NewDecryptResultValue(&exposedSecureValue2), + decryptedValuesResp := map[string]contracts.DecryptResult{ + "secure-value-1": contracts.NewDecryptResultValue(&exposedSecureValue1), + "secure-value-2": contracts.NewDecryptResultValue(&exposedSecureValue2), } decryptService := &OSSDecryptService{ @@ -75,9 +75,9 @@ func TestDecryptService(t *testing.T) { mockStorage.On("Decrypt", mock.Anything, xkube.Namespace("default"), "secure-value-2"). Return(secretv1beta1.ExposedSecureValue(""), mockErr) - decryptedValuesResp := map[string]service.DecryptResult{ - "secure-value-1": service.NewDecryptResultValue(&exposedSecureValue), - "secure-value-2": service.NewDecryptResultErr(mockErr), + decryptedValuesResp := map[string]contracts.DecryptResult{ + "secure-value-1": contracts.NewDecryptResultValue(&exposedSecureValue), + "secure-value-2": contracts.NewDecryptResultErr(mockErr), } decryptService := &OSSDecryptService{ diff --git a/pkg/registry/apis/secret/decrypt_client.go b/pkg/registry/apis/secret/decrypt_client.go new file mode 100644 index 00000000000..0ed0d3b31a7 --- /dev/null +++ b/pkg/registry/apis/secret/decrypt_client.go @@ -0,0 +1,25 @@ +package secret + +import ( + secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" + "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" +) + +// DecryptService is a decrypt client for secure value secrets. +type DecryptService = contracts.DecryptService + +var ( + ErrDecryptNotFound = contracts.ErrDecryptNotFound + ErrDecryptNotAuthorized = contracts.ErrDecryptNotAuthorized + ErrDecryptFailed = contracts.ErrDecryptFailed +) + +type DecryptResult = contracts.DecryptResult + +func NewDecryptResultErr(err error) DecryptResult { + return contracts.NewDecryptResultErr(err) +} + +func NewDecryptResultValue(value *secretv1beta1.ExposedSecureValue) DecryptResult { + return contracts.NewDecryptResultValue(value) +} diff --git a/pkg/registry/apis/secret/errors.go b/pkg/registry/apis/secret/errors.go deleted file mode 100644 index 62755f81a7c..00000000000 --- a/pkg/registry/apis/secret/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package secret - -import "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - -var ( - ErrSecureValueNotFound = contracts.ErrSecureValueNotFound - ErrSecureValueAlreadyExists = contracts.ErrSecureValueAlreadyExists -) diff --git a/pkg/registry/apis/secret/secure_value_client.go b/pkg/registry/apis/secret/secure_value_client.go index 3cb19a75c4c..e34f31abd42 100644 --- a/pkg/registry/apis/secret/secure_value_client.go +++ b/pkg/registry/apis/secret/secure_value_client.go @@ -23,6 +23,11 @@ import ( authsvc "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" ) +var ( + ErrSecureValueNotFound = contracts.ErrSecureValueNotFound + ErrSecureValueAlreadyExists = contracts.ErrSecureValueAlreadyExists +) + // SecureValueClient is a CRUD client for the secure value API. type SecureValueClient = contracts.SecureValueClient diff --git a/pkg/registry/apis/secret/service/decrypt.go b/pkg/registry/apis/secret/service/decrypt.go deleted file mode 100644 index f8ff83d95c9..00000000000 --- a/pkg/registry/apis/secret/service/decrypt.go +++ /dev/null @@ -1,36 +0,0 @@ -package service - -import ( - "context" - - secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" -) - -// DecryptResult is the (union) result of a decryption operation. -// It contains the decrypted `value` when the decryption succeeds, and the `err` when it fails. -// It is not possible to construct a `DecryptResult` where both `value` and `err` are set from another package. -type DecryptResult struct { - value *secretv1beta1.ExposedSecureValue - err error -} - -func (d DecryptResult) Error() error { - return d.err -} - -func (d DecryptResult) Value() *secretv1beta1.ExposedSecureValue { - return d.value -} - -func NewDecryptResultErr(err error) DecryptResult { - return DecryptResult{err: err} -} - -func NewDecryptResultValue(value *secretv1beta1.ExposedSecureValue) DecryptResult { - return DecryptResult{value: value} -} - -// DecryptService is the inferface for the decrypt service. -type DecryptService interface { - Decrypt(ctx context.Context, namespace string, names ...string) (map[string]DecryptResult, error) -} diff --git a/pkg/registry/apis/secret/testutils/testutils.go b/pkg/registry/apis/secret/testutils/testutils.go index 3b41784786f..478b9c63e84 100644 --- a/pkg/registry/apis/secret/testutils/testutils.go +++ b/pkg/registry/apis/secret/testutils/testutils.go @@ -140,7 +140,7 @@ type Sut struct { SecureValueService contracts.SecureValueService SecureValueMetadataStorage contracts.SecureValueMetadataStorage DecryptStorage contracts.DecryptStorage - DecryptService service.DecryptService + DecryptService contracts.DecryptService EncryptedValueStorage contracts.EncryptedValueStorage SQLKeeper *sqlkeeper.SQLKeeper Database *database.Database diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 7f9515b2a4f..9153346ef18 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -788,11 +788,11 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser if err != nil { return nil, err } - decryptService := decrypt.ProvideDecryptService(decryptStorage) - repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, decryptService) + v3 := decrypt.ProvideDecryptService(decryptStorage) + repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, v3) webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, repositorySecrets, factory, renderingService, resourceClient, eventualRestConfigProvider) - v3 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) - apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v3) + v4 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v4) if err != nil { return nil, err } @@ -1346,11 +1346,11 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface { if err != nil { return nil, err } - decryptService := decrypt.ProvideDecryptService(decryptStorage) - repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, decryptService) + v3 := decrypt.ProvideDecryptService(decryptStorage) + repositorySecrets := secrets.ProvideRepositorySecrets(featureToggles, secretsService, secureValueClient, v3) webhookExtraBuilder := webhooks.ProvideWebhooks(cfg, featureToggles, repositorySecrets, factory, renderingService, resourceClient, eventualRestConfigProvider) - v3 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) - apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v3) + v4 := extras.ProvideProvisioningOSSExtras(webhookExtraBuilder) + apiBuilder, err := provisioning2.RegisterAPIService(cfg, featureToggles, apiserverService, registerer, resourceClient, eventualRestConfigProvider, factory, accessClient, legacyMigrator, dualwriteService, usageStats, repositorySecrets, tracingService, v4) if err != nil { return nil, err } diff --git a/pkg/storage/secret/metadata/secure_value_test.go b/pkg/storage/secret/metadata/secure_value_test.go index 856c1dfc021..9400735cdd1 100644 --- a/pkg/storage/secret/metadata/secure_value_test.go +++ b/pkg/storage/secret/metadata/secure_value_test.go @@ -7,7 +7,6 @@ import ( secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts" - "github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/mitchellh/copystructure" @@ -111,24 +110,24 @@ func (m *model) list(namespace string) (*secretv1beta1.SecureValueList, error) { return &secretv1beta1.SecureValueList{Items: out}, nil } -func (m *model) decrypt(decrypter, namespace, name string) (map[string]service.DecryptResult, error) { +func (m *model) decrypt(decrypter, namespace, name string) (map[string]contracts.DecryptResult, error) { for _, v := range m.secureValues { if v.Namespace == namespace && v.Name == name && v.active { if slices.ContainsFunc(v.Spec.Decrypters, func(d string) bool { return d == decrypter }) { - return map[string]service.DecryptResult{ - name: service.NewDecryptResultValue(deepCopy(v).Spec.Value), + return map[string]contracts.DecryptResult{ + name: contracts.NewDecryptResultValue(deepCopy(v).Spec.Value), }, nil } - return map[string]service.DecryptResult{ - name: service.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized), + return map[string]contracts.DecryptResult{ + name: contracts.NewDecryptResultErr(contracts.ErrDecryptNotAuthorized), }, nil } } - return map[string]service.DecryptResult{ - name: service.NewDecryptResultErr(contracts.ErrDecryptNotFound), + return map[string]contracts.DecryptResult{ + name: contracts.NewDecryptResultErr(contracts.ErrDecryptNotFound), }, nil } From 24d898d6a7bf00b57f520837679776a1974df070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Wed, 23 Jul 2025 12:43:57 +0200 Subject: [PATCH 014/101] Provisioning: Fix flake in delete resources integration test (#108507) * Fix flake in delete resources test * Fix linting issue --- pkg/tests/apis/provisioning/helper_test.go | 6 ++- .../apis/provisioning/provisioning_test.go | 44 +++---------------- 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index bde59656092..d2787e68892 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -184,8 +184,12 @@ func (h *provisioningTestHelper) RenderObject(t *testing.T, filePath string, val // CopyToProvisioningPath copies a file to the provisioning path. // The from path is relative to test file's directory. func (h *provisioningTestHelper) CopyToProvisioningPath(t *testing.T, from, to string) { + fullPath := path.Join(h.ProvisioningPath, to) + err := os.MkdirAll(path.Dir(fullPath), 0750) + require.NoError(t, err, "failed to create directories for provisioning path") + file := h.LoadFile(from) - err := os.WriteFile(path.Join(h.ProvisioningPath, to), file, 0600) + err = os.WriteFile(fullPath, file, 0600) require.NoError(t, err, "failed to write file to provisioning path") } diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index e23552fd19d..7fa26a30fb6 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -743,46 +743,16 @@ func TestIntegrationProvisioning_DeleteResources(t *testing.T) { _, err := helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{}) require.NoError(t, err) - // create the structure: - // dashboard1.json - // folder/ - // dashboard2.json - // nested/ - // dashboard3.json - dashboard1 := helper.LoadFile("testdata/all-panels.json") - result := helper.AdminREST.Post(). - Namespace("default"). - Resource("repositories"). - Name(repo). - SubResource("files", "dashboard1.json"). - Body(dashboard1). - SetHeader("Content-Type", "application/json"). - Do(ctx) - require.NoError(t, result.Error()) - dashboard2 := helper.LoadFile("testdata/text-options.json") - result = helper.AdminREST.Post(). - Namespace("default"). - Resource("repositories"). - Name(repo). - SubResource("files", "folder", "dashboard2.json"). - Body(dashboard2). - SetHeader("Content-Type", "application/json"). - Do(ctx) - require.NoError(t, result.Error()) - dashboard3 := helper.LoadFile("testdata/timeline-demo.json") - result = helper.AdminREST.Post(). - Namespace("default"). - Resource("repositories"). - Name(repo). - SubResource("files", "folder", "nested", "dashboard3.json"). - Body(dashboard3). - SetHeader("Content-Type", "application/json"). - Do(ctx) - require.NoError(t, result.Error()) - + // Copy the dashboards to the repository path + helper.CopyToProvisioningPath(t, "testdata/all-panels.json", "dashboard1.json") + helper.CopyToProvisioningPath(t, "testdata/text-options.json", "folder/dashboard2.json") + helper.CopyToProvisioningPath(t, "testdata/timeline-demo.json", "folder/nested/dashboard3.json") // make sure we don't fail when there is a .keep file in a folder helper.CopyToProvisioningPath(t, "testdata/.keep", "folder/nested/.keep") + // Trigger and wait for a sync job to finish + helper.SyncAndWait(t, repo, nil) + dashboards, err := helper.DashboardsV1.Resource.List(ctx, metav1.ListOptions{}) require.NoError(t, err) require.Equal(t, 3, len(dashboards.Items)) From f2889c6cc4527303689c419bb3a98b3402adebcb Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 23 Jul 2025 14:25:11 +0300 Subject: [PATCH 015/101] Provisioning: Add provider icons (#108510) * Provisioning: Add provider icons * Use color logos * format * Move img to feature folder --- packages/grafana-data/src/types/icon.ts | 2 ++ public/app/core/icons/cached.json | 4 +++- .../GettingStarted/GettingStarted.tsx | 3 ++- .../provisioning/Shared/RepositoryTypeCards.tsx | 16 ++++++++++++++-- .../app/features/provisioning/img/bitbucket.svg | 16 ++++++++++++++++ public/app/features/provisioning/img/git.svg | 11 +++++++++++ public/app/features/provisioning/img/gitlab.svg | 10 ++++++++++ .../features/provisioning/img}/provisioning.svg | 0 .../provisioning/utils/repositoryTypes.ts | 11 +++++++++-- public/img/icons/unicons/bitbucket.svg | 5 +++++ public/img/icons/unicons/git.svg | 6 ++++++ 11 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 public/app/features/provisioning/img/bitbucket.svg create mode 100644 public/app/features/provisioning/img/git.svg create mode 100644 public/app/features/provisioning/img/gitlab.svg rename public/{img/provisioning => app/features/provisioning/img}/provisioning.svg (100%) create mode 100644 public/img/icons/unicons/bitbucket.svg create mode 100644 public/img/icons/unicons/git.svg diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 79df07aae5a..0cb4a61a25b 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -274,6 +274,8 @@ export const availableIconsIndex = { attach: true, 'dollar-alt': true, 'ai-sparkle': true, + bitbucket: true, + git: true, }; export type IconName = keyof typeof availableIconsIndex; diff --git a/public/app/core/icons/cached.json b/public/app/core/icons/cached.json index 520a7c14941..ce86bef4eae 100644 --- a/public/app/core/icons/cached.json +++ b/public/app/core/icons/cached.json @@ -180,5 +180,7 @@ "unicons/ai-sparkle", "unicons/dollar-alt", "unicons/window-grid", - "unicons/ban" + "unicons/ban", + "unicons/git", + "unicons/bitbucket" ] diff --git a/public/app/features/provisioning/GettingStarted/GettingStarted.tsx b/public/app/features/provisioning/GettingStarted/GettingStarted.tsx index 40116d38276..de986fe4748 100644 --- a/public/app/features/provisioning/GettingStarted/GettingStarted.tsx +++ b/public/app/features/provisioning/GettingStarted/GettingStarted.tsx @@ -7,7 +7,8 @@ import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Alert, Stack, useStyles2 } from '@grafana/ui'; import { Repository, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; -import provisioningSvg from 'img/provisioning/provisioning.svg'; + +import provisioningSvg from '../img/provisioning.svg'; import { EnhancedFeatures } from './EnhancedFeatures'; import { FeaturesList } from './FeaturesList'; diff --git a/public/app/features/provisioning/Shared/RepositoryTypeCards.tsx b/public/app/features/provisioning/Shared/RepositoryTypeCards.tsx index 3f0fd176069..7eab2d894a3 100644 --- a/public/app/features/provisioning/Shared/RepositoryTypeCards.tsx +++ b/public/app/features/provisioning/Shared/RepositoryTypeCards.tsx @@ -26,7 +26,11 @@ export function RepositoryTypeCards() { {gitProviders.map((config) => ( - + {config.logo ? ( + {config.label} + ) : ( + + )} ( - + {config.logo ? ( + {config.label} + ) : ( + + )} {config.type === 'local' ? ( Configure file provisioning ) : ( @@ -76,5 +84,9 @@ function getStyles() { card: css({ width: 220, }), + logo: css({ + width: 36, + height: 36, + }), }; } diff --git a/public/app/features/provisioning/img/bitbucket.svg b/public/app/features/provisioning/img/bitbucket.svg new file mode 100644 index 00000000000..d809d73b53c --- /dev/null +++ b/public/app/features/provisioning/img/bitbucket.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/public/app/features/provisioning/img/git.svg b/public/app/features/provisioning/img/git.svg new file mode 100644 index 00000000000..a22043d6ef3 --- /dev/null +++ b/public/app/features/provisioning/img/git.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/public/app/features/provisioning/img/gitlab.svg b/public/app/features/provisioning/img/gitlab.svg new file mode 100644 index 00000000000..fac27c2c322 --- /dev/null +++ b/public/app/features/provisioning/img/gitlab.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/public/img/provisioning/provisioning.svg b/public/app/features/provisioning/img/provisioning.svg similarity index 100% rename from public/img/provisioning/provisioning.svg rename to public/app/features/provisioning/img/provisioning.svg diff --git a/public/app/features/provisioning/utils/repositoryTypes.ts b/public/app/features/provisioning/utils/repositoryTypes.ts index 4ac255adc75..f8848044900 100644 --- a/public/app/features/provisioning/utils/repositoryTypes.ts +++ b/public/app/features/provisioning/utils/repositoryTypes.ts @@ -2,12 +2,16 @@ import { t } from '@grafana/i18n'; import { IconName } from '@grafana/ui'; import { RepoType } from '../Wizard/types'; +import bitbucketSvg from '../img/bitbucket.svg'; +import gitSvg from '../img/git.svg'; +import gitlabSvg from '../img/gitlab.svg'; export interface RepositoryTypeConfig { type: RepoType; label: string; description: string; icon: IconName; + logo?: string; } export const getRepositoryTypeConfigs = (): RepositoryTypeConfig[] => [ @@ -15,7 +19,8 @@ export const getRepositoryTypeConfigs = (): RepositoryTypeConfig[] => [ type: 'git', label: t('provisioning.repository-types.pure-git', 'Pure Git'), description: t('provisioning.repository-types.pure-git-description', 'Connect to any Git repository'), - icon: 'code-branch' as const, + icon: 'git' as const, + logo: gitSvg, }, { type: 'github', @@ -28,12 +33,14 @@ export const getRepositoryTypeConfigs = (): RepositoryTypeConfig[] => [ label: t('provisioning.repository-types.gitlab', 'GitLab'), description: t('provisioning.repository-types.gitlab-description', 'Connect to GitLab repositories'), icon: 'gitlab' as const, + logo: gitlabSvg, }, { type: 'bitbucket', label: t('provisioning.repository-types.bitbucket', 'Bitbucket'), description: t('provisioning.repository-types.bitbucket-description', 'Connect to Bitbucket repositories'), - icon: 'cloud' as const, + icon: 'bitbucket' as const, + logo: bitbucketSvg, }, { type: 'local', diff --git a/public/img/icons/unicons/bitbucket.svg b/public/img/icons/unicons/bitbucket.svg new file mode 100644 index 00000000000..c0995016e49 --- /dev/null +++ b/public/img/icons/unicons/bitbucket.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/img/icons/unicons/git.svg b/public/img/icons/unicons/git.svg new file mode 100644 index 00000000000..44d7438c613 --- /dev/null +++ b/public/img/icons/unicons/git.svg @@ -0,0 +1,6 @@ + + + + + From 06a2630ddd30250362d95fff8aef3d2ca854b6b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:31:36 +0100 Subject: [PATCH 016/101] Update dependency eslint to v9.31.0 (#108504) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-eslint-rules/package.json | 2 +- packages/grafana-plugin-configs/package.json | 2 +- yarn.lock | 89 +++++++++++--------- 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 54a1d756b6c..0078e8b89e4 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "esbuild": "0.25.8", "esbuild-loader": "4.3.0", "esbuild-plugin-browserslist": "^1.0.0", - "eslint": "9.28.0", + "eslint": "9.31.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "28.11.0", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 7b8cd3da539..35819b4ef6b 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@typescript-eslint/types": "^8.9.0", - "eslint": "9.28.0", + "eslint": "9.31.0", "tslib": "2.8.1" }, "private": true diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 14c9670fa1a..5213f1f91f9 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -15,7 +15,7 @@ "@types/eslint": "9.6.1", "@types/webpack-bundle-analyzer": "^4.7.0", "copy-webpack-plugin": "12.0.2", - "eslint": "9.28.0", + "eslint": "9.31.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", "glob": "11.0.3", diff --git a/yarn.lock b/yarn.lock index 7bcd86a7b5a..6f3baae2bf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2271,21 +2271,21 @@ __metadata: languageName: node linkType: hard -"@eslint/config-array@npm:^0.20.0": - version: 0.20.0 - resolution: "@eslint/config-array@npm:0.20.0" +"@eslint/config-array@npm:^0.21.0": + version: 0.21.0 + resolution: "@eslint/config-array@npm:0.21.0" dependencies: "@eslint/object-schema": "npm:^2.1.6" debug: "npm:^4.3.1" minimatch: "npm:^3.1.2" - checksum: 10/9db7f6cbb5363f2f98ee4805ce09d1a95c4349e86f3f456f2c23a0849b7a6aa8d2be4c25e376ee182af062762e15a101844881c89b566eea0856c481ffcb2090 + checksum: 10/f5a499e074ecf4b4a5efdca655418a12079d024b77d02fd35868eeb717c5bfdd8e32c6e8e1dd125330233a878026edda8062b13b4310169ba5bfee9623a67aa0 languageName: node linkType: hard -"@eslint/config-helpers@npm:^0.2.1": - version: 0.2.2 - resolution: "@eslint/config-helpers@npm:0.2.2" - checksum: 10/55dbb0b8d63c4cb28fa2a5fd5f16c785f6bd87eb0f50d2f42ec3f7d06b5c6201e2e170846a4360ca00105578b034fba132ed54e4ee3215be240c4a43e7839189 +"@eslint/config-helpers@npm:^0.3.0": + version: 0.3.0 + resolution: "@eslint/config-helpers@npm:0.3.0" + checksum: 10/b4c188f28cb8b76d4f4b49566ec1cc9d561bc888ef66ad34587151a212ff168afcf163493c72033149181f947cb950c3cca1525d7486303aae4dfde3e5399573 languageName: node linkType: hard @@ -2298,6 +2298,15 @@ __metadata: languageName: node linkType: hard +"@eslint/core@npm:^0.15.0": + version: 0.15.1 + resolution: "@eslint/core@npm:0.15.1" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10/f00062f0f18fbbfcf080315532340b01e18b729277245899844adb5bec3c9fe2991e1f134c633a15fdfbc4e8b631c2df167d241c49b37e02e937f8c22edfcd3a + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^3.3.1": version: 3.3.1 resolution: "@eslint/eslintrc@npm:3.3.1" @@ -2315,10 +2324,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.28.0": - version: 9.28.0 - resolution: "@eslint/js@npm:9.28.0" - checksum: 10/c2ab0416aef01f28f189525c9a86253f531852a1ed45db94a97f01bcc51d15720f38a8c201af7a7331eb1e80c991a7ef54c4be193b17540e48044672dba69bd8 +"@eslint/js@npm:9.31.0": + version: 9.31.0 + resolution: "@eslint/js@npm:9.31.0" + checksum: 10/83b45c707d9a6c62b79a9fb69b7ebcb31e8163647c0fdcae5972b4b2fcbbb1e8eac543986e0cca3582f8462a6587f6b6dc48e362b2ce3feb74282606c3d425ea languageName: node linkType: hard @@ -3210,7 +3219,7 @@ __metadata: dependencies: "@typescript-eslint/types": "npm:^8.9.0" "@typescript-eslint/utils": "npm:^8.9.0" - eslint: "npm:9.28.0" + eslint: "npm:9.31.0" tslib: "npm:2.8.1" languageName: unknown linkType: soft @@ -3426,7 +3435,7 @@ __metadata: "@types/eslint": "npm:9.6.1" "@types/webpack-bundle-analyzer": "npm:^4.7.0" copy-webpack-plugin: "npm:12.0.2" - eslint: "npm:9.28.0" + eslint: "npm:9.31.0" eslint-webpack-plugin: "npm:4.2.0" fork-ts-checker-webpack-plugin: "npm:9.0.2" glob: "npm:11.0.3" @@ -11000,12 +11009,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": - version: 8.14.0 - resolution: "acorn@npm:8.14.0" +"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.4.1, acorn@npm:^8.7.1, acorn@npm:^8.8.0, acorn@npm:^8.8.2": + version: 8.15.0 + resolution: "acorn@npm:8.15.0" bin: acorn: bin/acorn - checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 + checksum: 10/77f2de5051a631cf1729c090e5759148459cdb76b5f5c70f890503d629cf5052357b0ce783c0f976dd8a93c5150f59f6d18df1def3f502396a20f81282482fa4 languageName: node linkType: hard @@ -16052,13 +16061,13 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^8.1.0, eslint-scope@npm:^8.3.0": - version: 8.3.0 - resolution: "eslint-scope@npm:8.3.0" +"eslint-scope@npm:^8.1.0, eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10/ee1ff009e949423639a8b53453c0cb189967d9142c5d94dc3752bed9880140a0760007148ac6b0bd03557d70ede9cd7c3b1e66f9a7f3427b2dbeca2a5be22c91 + checksum: 10/e8e611701f65375e034c62123946e628894f0b54aa8cb11abe224816389abe5cd74cf16b62b72baa36504f22d1a958b9b8b0169b82397fe2e7997674c0d09b06 languageName: node linkType: hard @@ -16092,17 +16101,17 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.28.0": - version: 9.28.0 - resolution: "eslint@npm:9.28.0" +"eslint@npm:9.31.0": + version: 9.31.0 + resolution: "eslint@npm:9.31.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.20.0" - "@eslint/config-helpers": "npm:^0.2.1" - "@eslint/core": "npm:^0.14.0" + "@eslint/config-array": "npm:^0.21.0" + "@eslint/config-helpers": "npm:^0.3.0" + "@eslint/core": "npm:^0.15.0" "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.28.0" + "@eslint/js": "npm:9.31.0" "@eslint/plugin-kit": "npm:^0.3.1" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" @@ -16114,9 +16123,9 @@ __metadata: cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^8.3.0" - eslint-visitor-keys: "npm:^4.2.0" - espree: "npm:^10.3.0" + eslint-scope: "npm:^8.4.0" + eslint-visitor-keys: "npm:^4.2.1" + espree: "npm:^10.4.0" esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" @@ -16138,18 +16147,18 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10/0cce6dd3a53724d849253992477fd9b05bf6de3dd6bb761605e07beb14f63b6d2eb38f83aeb4a026ff2e3eeb687ac84c1d4a23a9f3186bc15564f5e75fed908c + checksum: 10/badfe1b62ee44dd389838ab27d6ca4f8a3159743a28c3145edd68af15db33fdacb13f096acdc2c559e9c8774d42fe3e1c513d5539d3297bfe8f342e3e4cfee33 languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.3.0": - version: 10.3.0 - resolution: "espree@npm:10.3.0" +"espree@npm:^10.0.1, espree@npm:^10.1.0, espree@npm:^10.3.0, espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" dependencies: - acorn: "npm:^8.14.0" + acorn: "npm:^8.15.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10/3412d44d4204c9e29d6b5dd0277400cfa0cd68495dc09eae1b9ce79d0c8985c1c5cc09cb9ba32a1cd963f48a49b0c46bdb7736afe395a300aa6bb1c0d86837e8 + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10/9b355b32dbd1cc9f57121d5ee3be258fab87ebeb7c83fc6c02e5af1a74fc8c5ba79fe8c663e69ea112c3e84a1b95e6a2067ac4443ee7813bb85ac7581acb8bf9 languageName: node linkType: hard @@ -18009,7 +18018,7 @@ __metadata: esbuild: "npm:0.25.8" esbuild-loader: "npm:4.3.0" esbuild-plugin-browserslist: "npm:^1.0.0" - eslint: "npm:9.28.0" + eslint: "npm:9.31.0" eslint-config-prettier: "npm:9.1.0" eslint-plugin-import: "npm:^2.31.0" eslint-plugin-jest: "npm:28.11.0" From 60df44bb2366fa79808dda46bb2657a2a1646307 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 23 Jul 2025 12:32:22 +0100 Subject: [PATCH 017/101] Chore: update CODEOWNERS (#108398) * remove joint ownership of root packages * continue separating CODEOWNERS * more CODEOWNERS, separate into package sections * more separation * few more CODEOWNER updates * index ownership * make frontend-platform owners of types * assign @grafana/schema ownership * update CODEOWNERS * define more CODEOWNERS * add more CODEOWNERs * update CODEOWNERS * update CODEOWNERS * update CODEOWNERS * update CODEOWNERS * add plugins platform to data/runtime interface * add featureToggles ownership * remove frontend-ops ownership --- .github/CODEOWNERS | 189 +++++++++++++++++- .../src/utils/namedColorsPalette.ts | 4 + 2 files changed, 185 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cf5e79de9f5..cdf2879eefd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -409,11 +409,92 @@ /e2e-playwright/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend # Packages -/packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/README.md @grafana/grafana-frontend-platform +/packages/rollup.config.parts.ts @grafana/frontend-ops + +# @grafana/alerting +/packages/grafana-alerting/ @grafana/alerting-frontend + +# @grafana/data +/packages/grafana-data/CHANGELOG.md @grafana/grafana-frontend-platform +/packages/grafana-data/LICENSE_APACHE2 @grafana/grafana-frontend-platform +/packages/grafana-data/README.md @grafana/grafana-frontend-platform +/packages/grafana-data/package.json @grafana/grafana-frontend-platform +/packages/grafana-data/project.json @grafana/grafana-frontend-platform +/packages/grafana-data/rollup.config.ts @grafana/grafana-frontend-platform +/packages/grafana-data/src/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-data/src/internal/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-data/src/unstable.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-data/tsconfig.build.json @grafana/grafana-frontend-platform +/packages/grafana-data/tsconfig.json @grafana/grafana-frontend-platform +/packages/grafana-data/test/ @grafana/grafana-frontend-platform +/packages/grafana-data/typings/ @grafana/grafana-frontend-platform + /packages/grafana-data/src/**/*logs* @grafana/observability-logs +/packages/grafana-data/src/context/plugins/ @grafana/plugins-platform-frontend +/packages/grafana-data/src/dataframe/ @grafana/dataviz-squad +/packages/grafana-data/src/datetime/ @grafana/grafana-frontend-platform +/packages/grafana-data/src/events/ @grafana/grafana-frontend-platform +/packages/grafana-data/src/field/ @grafana/dashboards-squad +/packages/grafana-data/src/geo/ @grafana/dataviz-squad +/packages/grafana-data/src/monaco/ @grafana/partner-datasources +/packages/grafana-data/src/panel/ @grafana/dashboards-squad +/packages/grafana-data/src/query/ @grafana/grafana-datasources-core-services +/packages/grafana-data/src/rbac/ @grafana/access-squad +/packages/grafana-data/src/table/ @grafana/dataviz-squad +/packages/grafana-data/src/text/ @grafana/dataviz-squad +/packages/grafana-data/src/themes/ @grafana/grafana-frontend-platform /packages/grafana-data/src/transformations/ @grafana/datapro +/packages/grafana-data/src/types/ @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/__snapshots__/ @grafanabot +/packages/grafana-data/src/utils/anyToNumber.ts @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/arrayUtils* @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/binaryOperators.ts @grafana/datapro +/packages/grafana-data/src/utils/csv* @grafana/dataviz-squad +/packages/grafana-data/src/utils/dataLinks* @grafana/dashboards-squad +/packages/grafana-data/src/utils/datasource.ts @grafana/grafana-datasources-core-services +/packages/grafana-data/src/utils/docs.ts @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/deprecationWarning* @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/featureToggles.ts @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/fieldParser.ts @grafana/dataviz-squad +/packages/grafana-data/src/utils/flotPairs* @grafana/dataviz-squad +/packages/grafana-data/src/utils/fuzzySearch* @grafana/grafana-search-navigate-organise +/packages/grafana-data/src/utils/labels* @grafana/observability-logs +/packages/grafana-data/src/utils/legend* @grafana/oss-big-tent +/packages/grafana-data/src/utils/location* @grafana/grafana-search-navigate-organise +/packages/grafana-data/src/utils/LocalStorageValueProvider.tsx @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/makeClassES5Compatible.ts @grafana/plugins-platform-frontend +/packages/grafana-data/src/utils/matchPluginId* @grafana/plugins-platform-frontend +/packages/grafana-data/src/utils/namedColorsPalette* @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/nodeGraph.ts @grafana/observability-traces-and-profiling +/packages/grafana-data/src/utils/numbers.ts @grafana/dataviz-squad +/packages/grafana-data/src/utils/object.ts @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/OptionsUIBuilders.ts @grafana/dashboards-squad +/packages/grafana-data/src/utils/Registry* @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/selectUtils.ts @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/series* @grafana/dataviz-squad +/packages/grafana-data/src/utils/store* @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/testdata/ @grafana/dataviz-squad +/packages/grafana-data/src/utils/tests/ @grafana/grafana-frontend-platform +/packages/grafana-data/src/utils/throwIfAngular* @grafana/plugins-platform-frontend +/packages/grafana-data/src/utils/unaryOperators.ts @grafana/datapro +/packages/grafana-data/src/utils/url* @grafana/grafana-search-navigate-organise +/packages/grafana-data/src/utils/valueMappings* @grafana/dashboards-squad +/packages/grafana-data/src/utils/variables.ts @grafana/plugins-platform-frontend +/packages/grafana-data/src/utils/withLoadingIndicator.ts @grafana/access-squad @grafana/alerting-frontend +/packages/grafana-data/src/valueFormats/ @grafana/dashboards-squad +/packages/grafana-data/src/vector/ @grafana/dataviz-squad + +# @grafana/e2e-selectors /packages/grafana-e2e-selectors/ @grafana/grafana-frontend-platform + +# @grafana/eslint-plugin +/packages/grafana-eslint-rules/ @grafana/grafana-frontend-platform + +# @grafana/flamegraph /packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling + +# @grafana/o11y-ds-frontend /packages/grafana-o11y-ds-frontend/ @grafana/observability-logs /packages/grafana-o11y-ds-frontend/src/IntervalInput/ @grafana/observability-traces-and-profiling /packages/grafana-o11y-ds-frontend/src/NodeGraph/ @grafana/observability-traces-and-profiling @@ -422,16 +503,104 @@ /packages/grafana-o11y-ds-frontend/src/TraceToLogs/ @grafana/observability-traces-and-profiling /packages/grafana-o11y-ds-frontend/src/TraceToMetrics/ @grafana/observability-traces-and-profiling /packages/grafana-o11y-ds-frontend/src/TraceToProfiles/ @grafana/observability-traces-and-profiling + +# @grafana/plugin-configs /packages/grafana-plugin-configs/ @grafana/plugins-platform-frontend + +# @grafana/prometheus /packages/grafana-prometheus/ @grafana/oss-big-tent -/packages/grafana-schema/src/**/*canvas* @grafana/dataviz-squad -/packages/grafana-schema/src/**/*tempo* @grafana/observability-traces-and-profiling + +# @grafana/runtime +/packages/grafana-runtime/CHANGELOG.md @grafana/grafana-frontend-platform +/packages/grafana-runtime/LICENSE_APACHE2 @grafana/grafana-frontend-platform +/packages/grafana-runtime/README.md @grafana/grafana-frontend-platform +/packages/grafana-runtime/package.json @grafana/grafana-frontend-platform +/packages/grafana-runtime/project.json @grafana/grafana-frontend-platform +/packages/grafana-runtime/rollup.config.ts @grafana/grafana-frontend-platform +/packages/grafana-runtime/src/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-runtime/src/internal/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-runtime/src/unstable.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend +/packages/grafana-runtime/tsconfig.build.json @grafana/grafana-frontend-platform +/packages/grafana-runtime/tsconfig.json @grafana/grafana-frontend-platform + +/packages/grafana-runtime/src/analytics @grafana/grafana-frontend-platform +/packages/grafana-runtime/src/analytics/plugins @grafana/plugins-platform-frontend +/packages/grafana-runtime/src/components/DataSourcePicker* @grafana/grafana-frontend-platform +/packages/grafana-runtime/src/components/EmbeddedDashboard.tsx @grafana/dashboards-squad +/packages/grafana-runtime/src/components/FolderPicker.tsx @grafana/grafana-frontend-platform +/packages/grafana-runtime/src/components/PanelRenderer.tsx @grafana/dashboards-squad +/packages/grafana-runtime/src/components/PanelDataErrorView.tsx @grafana/dashboards-squad +/packages/grafana-runtime/src/components/PluginPage.tsx @grafana/grafana-search-navigate-organise +/packages/grafana-runtime/src/components/QueryEditorWithMigration* @grafana/plugins-platform-frontend @grafana/plugins-platform-backend +/packages/grafana-runtime/src/config.ts @grafana/grafana-frontend-platform +/packages/grafana-runtime/src/services/ @grafana/grafana-frontend-platform +/packages/grafana-runtime/src/services/pluginExtensions @grafana/plugins-platform-frontend +/packages/grafana-runtime/src/services/CorrelationsService.ts @grafana/datapro +/packages/grafana-runtime/src/services/LocationService.test.tsx @grafana/grafana-search-navigate-organise +/packages/grafana-runtime/src/services/LocationService.tsx @grafana/grafana-search-navigate-organise +/packages/grafana-runtime/src/services/LocationSrv.ts @grafana/grafana-search-navigate-organise +/packages/grafana-runtime/src/services/live.ts @grafana/dashboards-squad +/packages/grafana-runtime/src/utils/chromeHeaderHeight.ts @grafana/grafana-search-navigate-organise +/packages/grafana-runtime/src/utils/DataSourceWithBackend* @grafana/grafana-datasources-core-services +/packages/grafana-runtime/src/utils/licensing.ts @grafana/grafana-operator-experience-squad +/packages/grafana-runtime/src/utils/logging.ts @grafana/grafana-frontend-platform +/packages/grafana-runtime/src/utils/migrationHandler* @grafana/plugins-platform-frontend @grafana/plugins-platform-backend +/packages/grafana-runtime/src/utils/plugin.ts @grafana/plugins-platform-frontend +/packages/grafana-runtime/src/utils/publicDashboardQueryHandler.ts @grafana/grafana-operator-experience-squad +/packages/grafana-runtime/src/utils/queryResponse* @grafana/grafana-datasources-core-services +/packages/grafana-runtime/src/utils/rbac.ts @grafana/identity-access-team +/packages/grafana-runtime/src/utils/returnToPrevious.ts @grafana/grafana-search-navigate-organise +/packages/grafana-runtime/src/utils/toDataQueryError.ts @grafana/grafana-datasources-core-services +/packages/grafana-runtime/src/utils/userStorage* @grafana/plugins-platform-frontend @grafana/grafana-frontend-platform + +# @grafana/schema +/packages/grafana-schema/ @grafana/grafana-app-platform-squad + +/packages/grafana-schema/src/**/annotationslist @grafana/dashboards-squad +/packages/grafana-schema/src/**/azuremonitor @grafana/partner-datasources +/packages/grafana-schema/src/**/barchart @grafana/dataviz-squad +/packages/grafana-schema/src/**/bargauge @grafana/dataviz-squad +/packages/grafana-schema/src/**/candlestick @grafana/dataviz-squad +/packages/grafana-schema/src/**/canvas @grafana/dataviz-squad +/packages/grafana-schema/src/**/cloudwatch @grafana/aws-datasources +/packages/grafana-schema/src/**/dashboard @grafana/dashboards-squad +/packages/grafana-schema/src/**/dashboardlist @grafana/grafana-search-navigate-organise +/packages/grafana-schema/src/**/datagrid @grafana/dataviz-squad +/packages/grafana-schema/src/**/debug @ryantxu +/packages/grafana-schema/src/**/elasticsearch @grafana/partner-datasources +/packages/grafana-schema/src/**/gauge @grafana/dataviz-squad +/packages/grafana-schema/src/**/geomap @grafana/dataviz-squad +/packages/grafana-schema/src/**/googlecloudmonitoring @grafana/partner-datasources +/packages/grafana-schema/src/**/grafanapyroscope @grafana/observability-traces-and-profiling +/packages/grafana-schema/src/**/heatmap @grafana/dataviz-squad +/packages/grafana-schema/src/**/histogram @grafana/dataviz-squad +/packages/grafana-schema/src/**/librarypanel @grafana/sharing-squad +/packages/grafana-schema/src/**/logs @grafana/observability-logs +/packages/grafana-schema/src/**/logsnew @grafana/observability-logs +/packages/grafana-schema/src/**/loki @grafana/oss-big-tent @grafana/observability-logs +/packages/grafana-schema/src/**/news @grafana/dataviz-squad +/packages/grafana-schema/src/**/nodegraph @grafana/observability-traces-and-profiling @grafana/app-o11y-visualizations +/packages/grafana-schema/src/**/parca @grafana/oss-big-tent +/packages/grafana-schema/src/**/piechart @grafana/dataviz-squad +/packages/grafana-schema/src/**/publicdashboard @grafana/sharing-squad +/packages/grafana-schema/src/**/stat @grafana/dataviz-squad +/packages/grafana-schema/src/**/statetimeline @grafana/dataviz-squad +/packages/grafana-schema/src/**/statushistory @grafana/dataviz-squad +/packages/grafana-schema/src/**/table @grafana/dataviz-squad +/packages/grafana-schema/src/**/tempo @grafana/observability-traces-and-profiling +/packages/grafana-schema/src/**/text @grafana/dataviz-squad +/packages/grafana-schema/src/**/timeseries @grafana/dataviz-squad +/packages/grafana-schema/src/**/trend @grafana/dataviz-squad +/packages/grafana-schema/src/**/xychart @grafana/dataviz-squad +/packages/grafana-schema/src/**/xychart2 @grafana/dataviz-squad + +# @grafana/sql /packages/grafana-sql/ @grafana/partner-datasources @grafana/oss-big-tent -/packages/grafana-ui/.storybook/ @grafana/grafana-frontend-platform -/packages/grafana-ui/src/components/ @grafana/grafana-frontend-platform + +# @grafana/ui +/packages/grafana-ui/ @grafana/grafana-frontend-platform /packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/DateTimePickers/ @grafana/grafana-frontend-platform /packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/PluginSignatureBadge/ @grafana/plugins-platform-frontend /packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations @@ -446,9 +615,13 @@ /packages/grafana-ui/src/graveyard/Graph/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad -/packages/grafana-ui/src/utils/storybook/ @grafana/grafana-frontend-platform -/packages/grafana-alerting/ @grafana/alerting-frontend +/packages/grafana-ui/tsconfig.build.json @grafana/frontend-ops +/packages/grafana-ui/tsconfig.json @grafana/frontend-ops + +# @grafana/i18n /packages/grafana-i18n/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend + +# @grafana/test-utils /packages/grafana-test-utils @grafana/grafana-frontend-platform # root files, mostly frontend diff --git a/packages/grafana-data/src/utils/namedColorsPalette.ts b/packages/grafana-data/src/utils/namedColorsPalette.ts index 6d798ec96af..ab47afbb115 100644 --- a/packages/grafana-data/src/utils/namedColorsPalette.ts +++ b/packages/grafana-data/src/utils/namedColorsPalette.ts @@ -1,3 +1,7 @@ +/** + * @deprecated + * This is defined on the theme object now. Use `theme.visualization.palette` instead. + */ export const classicColors = [ '#7EB26D', // 0: pale green '#EAB839', // 1: mustard From c43a92c1be403565de1ffd33dfce34e0b07d6e8f Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Wed, 23 Jul 2025 14:33:06 +0200 Subject: [PATCH 018/101] Choe: add id-token to fetch secrets (#108506) * add PR author * adding id-token permissions --- .github/workflows/issue-opened.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml index 3dc7cbad7f8..11aec26b87f 100644 --- a/.github/workflows/issue-opened.yml +++ b/.github/workflows/issue-opened.yml @@ -123,6 +123,7 @@ jobs: runs-on: ubuntu-latest permissions: issues: write + id-token: write steps: - name: "Get vault secrets" id: vault-secrets From 7c872f0e8aa6d05061f546da98d94fe223192fa2 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:38:09 +0200 Subject: [PATCH 019/101] Alerting: Add feedback buttons for the new AI helpers (#108207) * Add alerting ai buttons for cloud * add tracking for ai buttons usage * Empty commit to trigger GitHub Actions * wip * add FrontendOnly in ff * update analytics folder * prettier * revert ff being frontend only * add feedback button in templates * review comments * address some review comments * refactor * revert change and remove comment * prettier * update betterer * remove unused property * prettier * update AIFeedbackComponent * add feedback buttons for alert rule generation with ai * remove tracking from oss * fix analytics imports * udpdate changes in go.mod go.sum * prettier * update translations * update workspace * fix betterer * address pr comments * move most of the ai feedback buttons code to enterprise * prettier * fix * address pr feedback --- .../src/types/featureToggles.gen.ts | 5 +++ pkg/services/featuremgmt/registry.go | 9 ++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/featuremgmt/toggles_gen.json | 15 +++++++ .../components/receivers/TemplateForm.tsx | 6 +++ .../components/receivers/TemplatePreview.tsx | 38 +++++++++++++++- .../alert-rule-form/AlertRuleForm.tsx | 5 +++ .../AI/addAIFeedbackButton.tsx | 43 +++++++++++++++++++ public/locales/en-US/grafana.json | 2 + 10 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 534d8064bbc..3b9875c801e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -748,6 +748,11 @@ export interface FeatureToggles { */ alertingAIGenAlertRules?: boolean; /** + * Enable AI-generated feedback from the Grafana UI. + * @default false + */ + alertingAIFeedback?: boolean; + /** * Enable AI-improve alert rules labels and annotations. * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3738ab4877a..d91e76998e2 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1278,6 +1278,15 @@ var ( HideFromDocs: true, Expression: "false", }, + { + Name: "alertingAIFeedback", + Description: "Enable AI-generated feedback from the Grafana UI.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromAdminPage: true, + HideFromDocs: true, + Expression: "false", + }, { Name: "alertingAIImproveAlertRules", Description: "Enable AI-improve alert rules labels and annotations.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 74b5f2a6fd9..689b0883024 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -167,6 +167,7 @@ crashDetection,experimental,@grafana/observability-traces-and-profiling,false,fa alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false alertingAIGenAlertRules,experimental,@grafana/alerting-squad,false,false,false +alertingAIFeedback,experimental,@grafana/alerting-squad,false,false,false alertingAIImproveAlertRules,experimental,@grafana/alerting-squad,false,false,false alertingAIGenTemplates,experimental,@grafana/alerting-squad,false,false,false alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index c94945edc9a..5ea9f0f1995 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -679,6 +679,10 @@ const ( // Enable AI-generated alert rules. FlagAlertingAIGenAlertRules = "alertingAIGenAlertRules" + // FlagAlertingAIFeedback + // Enable AI-generated feedback from the Grafana UI. + FlagAlertingAIFeedback = "alertingAIFeedback" + // FlagAlertingAIImproveAlertRules // Enable AI-improve alert rules labels and annotations. FlagAlertingAIImproveAlertRules = "alertingAIImproveAlertRules" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index a7cc676d47a..2b3a1c27cf9 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -89,6 +89,21 @@ "expression": "false" } }, + { + "metadata": { + "name": "alertingAIFeedback", + "resourceVersion": "1752820131286", + "creationTimestamp": "2025-07-18T06:28:51Z" + }, + "spec": { + "description": "Enable AI-generated feedback from the Grafana UI.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true, + "hideFromDocs": true, + "expression": "false" + } + }, { "metadata": { "name": "alertingAIGenAlertRules", diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 0d104bd528f..6f0f358f011 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -107,6 +107,9 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) const [payload, setPayload] = useState(defaultPayloadString); const [payloadFormatError, setPayloadFormatError] = useState(null); + // AI feedback state + const [aiGeneratedTemplate, setAiGeneratedTemplate] = useState(false); + const { isProvisioned } = useNotificationTemplateMetadata(originalTemplate); const originalTemplatePrefill: TemplateFormValues | undefined = originalTemplate ? { title: originalTemplate.title, content: originalTemplate.content } @@ -167,6 +170,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) const handleTemplateGenerated = (template: string) => { setValue('content', template); + setAiGeneratedTemplate(true); }; return ( @@ -352,6 +356,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) setPayloadFormatError={setPayloadFormatError} payloadFormatError={payloadFormatError} className={cx(styles.templatePreview, styles.minEditorSize)} + aiGeneratedTemplate={aiGeneratedTemplate} + setAiGeneratedTemplate={setAiGeneratedTemplate} />
)} diff --git a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx index 8745e12effd..fc6014a0e0e 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx @@ -8,6 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { Alert, Box, Button, CodeEditor, useStyles2 } from '@grafana/ui'; import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi'; +import { AIFeedbackButtonComponent } from '../../enterprise-components/AI/addAIFeedbackButton'; import { stringifyErrorLike } from '../../utils/misc'; import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader'; @@ -20,6 +21,8 @@ export function TemplatePreview({ payloadFormatError, setPayloadFormatError, className, + aiGeneratedTemplate, + setAiGeneratedTemplate, }: { payload: string; templateName: string; @@ -27,6 +30,8 @@ export function TemplatePreview({ payloadFormatError: string | null; setPayloadFormatError: (value: React.SetStateAction) => void; className?: string; + aiGeneratedTemplate?: boolean; + setAiGeneratedTemplate?: (aiGeneratedTemplate: boolean) => void; }) { const styles = useStyles2(getStyles); @@ -48,7 +53,10 @@ export function TemplatePreview({ disabled={isLoading} icon="sync" aria-label={t('alerting.template-preview.aria-label-refresh-preview', 'Refresh preview')} - onClick={onPreview} + onClick={() => { + onPreview(); + setAiGeneratedTemplate?.(false); + }} size="sm" variant="secondary" > @@ -56,6 +64,9 @@ export function TemplatePreview({ } /> +
+ +
{({ height }) =>
{previewToRender}
} @@ -163,6 +174,22 @@ const getStyles = (theme: GrafanaTheme2) => ({ errorText: css({ color: theme.colors.error.text, }), + feedbackContainer: css({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderTop: `1px solid ${theme.colors.border.medium}`, + backgroundColor: theme.colors.background.secondary, + minHeight: 'auto', + }), + emptyState: css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + }), }, }); @@ -174,10 +201,12 @@ export function getPreviewResults( // ERRORS IN JSON OR IN REQUEST (endpoint not available, for example) const previewErrorRequest = previewError ? stringifyErrorLike(previewError) : undefined; const errorToRender = payloadFormatError || previewErrorRequest; + const styles = useStyles2(getStyles); //PREVIEW : RESULTS AND ERRORS const previewResponseResults = data?.results ?? []; const previewResponseErrors = data?.errors; + const hasContent = previewResponseResults.length > 0 || previewResponseErrors || errorToRender; return ( <> @@ -187,7 +216,12 @@ export function getPreviewResults(
)} {previewResponseErrors && } - {previewResponseResults && } + {previewResponseResults.length > 0 && } + {!hasContent && ( +
+ Add template content to see preview +
+ )} ); } diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 0a16d0022a0..3d70f60f79a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -40,6 +40,7 @@ import { RulerGroupUpdatedResponse, isGrafanaGroupUpdatedResponse, } from '../../../api/alertRuleModel'; +import { AIFeedbackButtonComponent } from '../../../enterprise-components/AI/addAIFeedbackButton'; import { useAddRuleToRuleGroup, useUpdateRuleInRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup'; import { defaultFormValuesForRuleType, @@ -244,6 +245,10 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) => {/* Step 1 */} + + {/* AI Feedback: automatically shown for rules generated by AI */} + + {/* Step 2 */} {/* Step 3-4-5 */} diff --git a/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx b/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx new file mode 100644 index 00000000000..fae71645554 --- /dev/null +++ b/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx @@ -0,0 +1,43 @@ +import { ComponentType, createElement } from 'react'; + +import { t } from '@grafana/i18n'; +import { withErrorBoundary } from '@grafana/ui'; +import { AIFeedbackOrigin } from 'app/extensions/alerting/AI/feedback/AIFeedbackComponent'; + +import { logError } from '../../Analytics'; + +export interface GenAIFeedbackButtonProps { + origin: AIFeedbackOrigin; + shouldShowFeedbackButton?: boolean; + // If true, the component will use the route detection to determine if the feedback should be shown + // this is necessary for example in alerting rule form, where the feedback button is shown in the same page as the rule form + useRouteDetection?: boolean; +} + +let InternalAIFeedbackButtonComponent: ComponentType | null = null; + +export const AIFeedbackButtonComponent: ComponentType = (props) => { + if (!InternalAIFeedbackButtonComponent) { + return null; + } + + // Wrap the component with error boundary + const WrappedComponent = withErrorBoundary(InternalAIFeedbackButtonComponent, { + title: t('alerting.ai.error-boundary.feedback-button', 'AI Feedback Button failed to load'), + style: 'alertbox', + errorLogger: logError, + }); + + // Provide default value for shouldShowFeedbackButton + const propsWithDefaults = { + shouldShowFeedbackButton: true, + useRouteDetection: false, + ...props, + }; + + return createElement(WrappedComponent, propsWithDefaults); +}; + +export function addAIFeedbackButton(component: ComponentType | null) { + InternalAIFeedbackButtonComponent = component; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 180039f2b38..2d21e3f49bf 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "AI Alert Rule Button failed to load", + "feedback-button": "AI Feedback Button failed to load", "improve-annotations-button": "AI Improve Annotations Button failed to load", "improve-labels-button": "AI Improve Labels Button failed to load", "template-button": "AI Template Button failed to load", @@ -2803,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Refresh preview", + "empty-state": "Add template content to see preview", "label-preview": "Preview", "refresh": "Refresh" }, From b99e9484d29ebcd62e89f9baa64922219a9a235d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20V=C4=93rzemnieks?= Date: Wed, 23 Jul 2025 06:15:44 -0700 Subject: [PATCH 020/101] CloudWatch: Fix broken test (#108520) --- pkg/tsdb/cloudwatch/time_series_query_test.go | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pkg/tsdb/cloudwatch/time_series_query_test.go b/pkg/tsdb/cloudwatch/time_series_query_test.go index 9f269bd4347..b1f89bb6e6f 100644 --- a/pkg/tsdb/cloudwatch/time_series_query_test.go +++ b/pkg/tsdb/cloudwatch/time_series_query_test.go @@ -138,9 +138,6 @@ func TestTimeSeriesQuery(t *testing.T) { func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMetricData_is_called_once_per_grouping_of_queries_by_region(t *testing.T) { /* TODO: This test aims to verify the logic to group regions which has been extracted from ParseMetricDataQueries. It should be replaced by a test at a lower level when grouping by regions is incorporated into a separate business logic layer */ - // FIXME: this test is broken - it only works because we're recovering from the panic that the Mock - // produces - see time_series_query.go line 78. If that recover is commented out, the test fails. - t.Skip("skipping broken test") ds := newTestDatasource() origNewCWClient := NewCWClient @@ -152,6 +149,8 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe NewCWClient = func(aws.Config) models.CWClient { return &mockMetricClient } + oneToTwoHoursAgo := backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)} + twoToThreeHoursAgo := backend.TimeRange{From: time.Now().Add(time.Hour * -3), To: time.Now().Add(time.Hour * -2)} t.Run("Queries with the same region should call GetMetricData 1 time", func(t *testing.T) { mockMetricClient = mocks.MetricsAPI{} @@ -164,7 +163,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe Queries: []backend.DataQuery{ { RefID: "A", - TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + TimeRange: oneToTwoHoursAgo, JSON: json.RawMessage(`{ "type": "timeSeriesQuery", "namespace": "AWS/EC2", @@ -176,7 +175,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe }, { RefID: "B", - TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + TimeRange: oneToTwoHoursAgo, JSON: json.RawMessage(`{ "type": "timeSeriesQuery", "namespace": "AWS/EC2", @@ -206,7 +205,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe Queries: []backend.DataQuery{ { RefID: "A", - TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + TimeRange: oneToTwoHoursAgo, JSON: json.RawMessage(`{ "type": "timeSeriesQuery", "namespace": "AWS/EC2", @@ -218,7 +217,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe }, { RefID: "A2", - TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + TimeRange: oneToTwoHoursAgo, JSON: json.RawMessage(`{ "type": "timeSeriesQuery", "namespace": "AWS/EC2", @@ -230,7 +229,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe }, { RefID: "B", - TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + TimeRange: oneToTwoHoursAgo, JSON: json.RawMessage(`{ "type": "timeSeriesQuery", "namespace": "AWS/EC2", @@ -260,7 +259,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe Queries: []backend.DataQuery{ { RefID: "A", - TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now()}, + TimeRange: twoToThreeHoursAgo, JSON: json.RawMessage(`{ "type": "timeSeriesQuery", "namespace": "AWS/EC2", @@ -272,7 +271,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe }, { RefID: "A2", - TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now()}, + TimeRange: twoToThreeHoursAgo, JSON: json.RawMessage(`{ "type": "timeSeriesQuery", "namespace": "AWS/EC2", @@ -284,7 +283,7 @@ func Test_executeTimeSeriesQuery_getCWClient_is_called_once_per_region_and_GetMe }, { RefID: "B", - TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + TimeRange: oneToTwoHoursAgo, JSON: json.RawMessage(`{ "type": "timeSeriesQuery", "namespace": "AWS/EC2", From 9f212d11d0ac9c38ada62a7db830844bb9b02905 Mon Sep 17 00:00:00 2001 From: Timur Olzhabayev Date: Wed, 23 Jul 2025 16:00:12 +0200 Subject: [PATCH 021/101] Chore: bump github action version (#108522) bump github action version --- .github/workflows/issue-opened.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml index 11aec26b87f..80b853d0c69 100644 --- a/.github/workflows/issue-opened.yml +++ b/.github/workflows/issue-opened.yml @@ -135,13 +135,14 @@ jobs: GITHUB_APP_PRIVATE_KEY=plugins_platform_issue_triager_github_bot:app_pem - name: Generate token id: generate_token - uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ env.GITHUB_APP_ID }} private-key: ${{ env.GITHUB_APP_PRIVATE_KEY }} permission-members: read - name: Check if member of grafana org id: check-if-grafana-org-member + continue-on-error: true run: gh api https://api.github.com/orgs/grafana/members/${{ env.ACTOR }} >/dev/null 2>&1 && echo "is_grafana_org_member=true" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ steps.generate_token.outputs.token }} From 0483544b06220539e9927b2c79500718c2ecfd81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:34:02 +0100 Subject: [PATCH 022/101] Update dependency eslint-plugin-import to v2.32.0 (#108518) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 192 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 108 insertions(+), 84 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6f3baae2bf7..dc03bc3f24b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11405,17 +11405,19 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": - version: 3.1.8 - resolution: "array-includes@npm:3.1.8" +"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9": + version: 3.1.9 + resolution: "array-includes@npm:3.1.9" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" - es-object-atoms: "npm:^1.0.0" - get-intrinsic: "npm:^1.2.4" - is-string: "npm:^1.0.7" - checksum: 10/290b206c9451f181fb2b1f79a3bf1c0b66bb259791290ffbada760c79b284eef6f5ae2aeb4bcff450ebc9690edd25732c4c73a3c2b340fcc0f4563aed83bf488 + es-abstract: "npm:^1.24.0" + es-object-atoms: "npm:^1.1.1" + get-intrinsic: "npm:^1.3.0" + is-string: "npm:^1.1.1" + math-intrinsics: "npm:^1.1.0" + checksum: 10/8bfe9a58df74f326b4a76b04ee05c13d871759e888b4ee8f013145297cf5eb3c02cfa216067ebdaac5d74eb9763ac5cad77cdf2773b8ab475833701e032173aa languageName: node linkType: hard @@ -11463,29 +11465,30 @@ __metadata: languageName: node linkType: hard -"array.prototype.findlastindex@npm:^1.2.5": - version: 1.2.5 - resolution: "array.prototype.findlastindex@npm:1.2.5" +"array.prototype.findlastindex@npm:^1.2.6": + version: 1.2.6 + resolution: "array.prototype.findlastindex@npm:1.2.6" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" + es-abstract: "npm:^1.23.9" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10/7c5c821f357cd53ab6cc305de8086430dd8d7a2485db87b13f843e868055e9582b1fd338f02338f67fc3a1603ceaf9610dd2a470b0b506f9d18934780f95b246 + es-object-atoms: "npm:^1.1.1" + es-shim-unscopables: "npm:^1.1.0" + checksum: 10/5ddb6420e820bef6ddfdcc08ce780d0fd5e627e97457919c27e32359916de5a11ce12f7c55073555e503856618eaaa70845d6ca11dcba724766f38eb1c22f7a2 languageName: node linkType: hard -"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": - version: 1.3.2 - resolution: "array.prototype.flat@npm:1.3.2" +"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.3": + version: 1.3.3 + resolution: "array.prototype.flat@npm:1.3.3" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - es-shim-unscopables: "npm:^1.0.0" - checksum: 10/d9d2f6f27584de92ec7995bc931103e6de722cd2498bdbfc4cba814fc3e52f056050a93be883018811f7c0a35875f5056584a0e940603a5e5934f0279896aebe + call-bind: "npm:^1.0.8" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.5" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10/f9b992fa0775d8f7c97abc91eb7f7b2f0ed8430dd9aeb9fdc2967ac4760cdd7fc2ef7ead6528fef40c7261e4d790e117808ce0d3e7e89e91514d4963a531cd01 languageName: node linkType: hard @@ -14731,7 +14734,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -15455,26 +15458,26 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.17.5, es-abstract@npm:^1.22.1, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9": - version: 1.23.9 - resolution: "es-abstract@npm:1.23.9" +"es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": + version: 1.24.0 + resolution: "es-abstract@npm:1.24.0" dependencies: array-buffer-byte-length: "npm:^1.0.2" arraybuffer.prototype.slice: "npm:^1.0.4" available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" + call-bound: "npm:^1.0.4" data-view-buffer: "npm:^1.0.2" data-view-byte-length: "npm:^1.0.2" data-view-byte-offset: "npm:^1.0.1" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" + es-object-atoms: "npm:^1.1.1" es-set-tostringtag: "npm:^2.1.0" es-to-primitive: "npm:^1.3.0" function.prototype.name: "npm:^1.1.8" - get-intrinsic: "npm:^1.2.7" - get-proto: "npm:^1.0.0" + get-intrinsic: "npm:^1.3.0" + get-proto: "npm:^1.0.1" get-symbol-description: "npm:^1.1.0" globalthis: "npm:^1.0.4" gopd: "npm:^1.2.0" @@ -15486,21 +15489,24 @@ __metadata: is-array-buffer: "npm:^3.0.5" is-callable: "npm:^1.2.7" is-data-view: "npm:^1.0.2" + is-negative-zero: "npm:^2.0.3" is-regex: "npm:^1.2.1" + is-set: "npm:^2.0.3" is-shared-array-buffer: "npm:^1.0.4" is-string: "npm:^1.1.1" is-typed-array: "npm:^1.1.15" - is-weakref: "npm:^1.1.0" + is-weakref: "npm:^1.1.1" math-intrinsics: "npm:^1.1.0" - object-inspect: "npm:^1.13.3" + object-inspect: "npm:^1.13.4" object-keys: "npm:^1.1.1" object.assign: "npm:^4.1.7" own-keys: "npm:^1.0.1" - regexp.prototype.flags: "npm:^1.5.3" + regexp.prototype.flags: "npm:^1.5.4" safe-array-concat: "npm:^1.1.3" safe-push-apply: "npm:^1.0.0" safe-regex-test: "npm:^1.1.0" set-proto: "npm:^1.0.0" + stop-iteration-iterator: "npm:^1.1.0" string.prototype.trim: "npm:^1.2.10" string.prototype.trimend: "npm:^1.0.9" string.prototype.trimstart: "npm:^1.0.8" @@ -15509,8 +15515,8 @@ __metadata: typed-array-byte-offset: "npm:^1.0.4" typed-array-length: "npm:^1.0.7" unbox-primitive: "npm:^1.1.0" - which-typed-array: "npm:^1.1.18" - checksum: 10/31a321966d760d88fc2ed984104841b42f4f24fc322b246002b9be0af162e03803ee41fcc3cf8be89e07a27ba3033168f877dd983703cb81422ffe5322a27582 + which-typed-array: "npm:^1.1.19" + checksum: 10/64e07a886f7439cf5ccfc100f9716e6173e10af6071a50a5031afbdde474a3dbc9619d5965da54e55f8908746a9134a46be02af8c732d574b7b81ed3124e2daf languageName: node linkType: hard @@ -15580,12 +15586,12 @@ __metadata: languageName: node linkType: hard -"es-shim-unscopables@npm:^1.0.0, es-shim-unscopables@npm:^1.0.2": - version: 1.0.2 - resolution: "es-shim-unscopables@npm:1.0.2" +"es-shim-unscopables@npm:^1.0.2, es-shim-unscopables@npm:^1.1.0": + version: 1.1.0 + resolution: "es-shim-unscopables@npm:1.1.0" dependencies: - hasown: "npm:^2.0.0" - checksum: 10/6d3bf91f658a27cc7217cd32b407a0d714393a84d125ad576319b9e83a893bea165cf41270c29e9ceaa56d3cf41608945d7e2a2c31fd51c0009b0c31402b91c7 + hasown: "npm:^2.0.2" + checksum: 10/c351f586c30bbabc62355be49564b2435468b52c3532b8a1663672e3d10dc300197e69c247869dd173e56d86423ab95fc0c10b0939cdae597094e0fdca078cba languageName: node linkType: hard @@ -15835,44 +15841,44 @@ __metadata: languageName: node linkType: hard -"eslint-module-utils@npm:^2.12.0": - version: 2.12.0 - resolution: "eslint-module-utils@npm:2.12.0" +"eslint-module-utils@npm:^2.12.1": + version: 2.12.1 + resolution: "eslint-module-utils@npm:2.12.1" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee + checksum: 10/bd25d6610ec3abaa50e8f1beb0119541562bbb8dd02c035c7e887976fe1e0c5dd8175f4607ca8d86d1146df24d52a071bd3d1dd329f6902bd58df805a8ca16d3 languageName: node linkType: hard "eslint-plugin-import@npm:^2.31.0": - version: 2.31.0 - resolution: "eslint-plugin-import@npm:2.31.0" + version: 2.32.0 + resolution: "eslint-plugin-import@npm:2.32.0" dependencies: "@rtsao/scc": "npm:^1.1.0" - array-includes: "npm:^3.1.8" - array.prototype.findlastindex: "npm:^1.2.5" - array.prototype.flat: "npm:^1.3.2" - array.prototype.flatmap: "npm:^1.3.2" + array-includes: "npm:^3.1.9" + array.prototype.findlastindex: "npm:^1.2.6" + array.prototype.flat: "npm:^1.3.3" + array.prototype.flatmap: "npm:^1.3.3" debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.12.0" + eslint-module-utils: "npm:^2.12.1" hasown: "npm:^2.0.2" - is-core-module: "npm:^2.15.1" + is-core-module: "npm:^2.16.1" is-glob: "npm:^4.0.3" minimatch: "npm:^3.1.2" object.fromentries: "npm:^2.0.8" object.groupby: "npm:^1.0.3" - object.values: "npm:^1.2.0" + object.values: "npm:^1.2.1" semver: "npm:^6.3.1" - string.prototype.trimend: "npm:^1.0.8" + string.prototype.trimend: "npm:^1.0.9" tsconfig-paths: "npm:^3.15.0" peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - checksum: 10/6b76bd009ac2db0615d9019699d18e2a51a86cb8c1d0855a35fb1b418be23b40239e6debdc6e8c92c59f1468ed0ea8d7b85c817117a113d5cc225be8a02ad31c + checksum: 10/1bacf4967e9ebf99e12176a795f0d6d3a87d1c9a030c2207f27b267e10d96a1220be2647504c7fc13ab543cdf13ffef4b8f5620e0447032dba4ff0d3922f7c9e languageName: node linkType: hard @@ -16955,12 +16961,12 @@ __metadata: languageName: node linkType: hard -"for-each@npm:^0.3.3": - version: 0.3.3 - resolution: "for-each@npm:0.3.3" +"for-each@npm:^0.3.3, for-each@npm:^0.3.5": + version: 0.3.5 + resolution: "for-each@npm:0.3.5" dependencies: - is-callable: "npm:^1.1.3" - checksum: 10/fdac0cde1be35610bd635ae958422e8ce0cc1313e8d32ea6d34cfda7b60850940c1fd07c36456ad76bd9c24aef6ff5e03b02beb58c83af5ef6c968a64eada676 + is-callable: "npm:^1.2.7" + checksum: 10/330cc2439f85c94f4609de3ee1d32c5693ae15cdd7fe3d112c4fd9efd4ce7143f2c64ef6c2c9e0cfdb0058437f33ef05b5bdae5b98fcc903fb2143fbaf0fea0f languageName: node linkType: hard @@ -19410,7 +19416,7 @@ __metadata: languageName: node linkType: hard -"is-callable@npm:^1.1.3, is-callable@npm:^1.2.7": +"is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" checksum: 10/48a9297fb92c99e9df48706241a189da362bff3003354aea4048bd5f7b2eb0d823cd16d0a383cece3d76166ba16d85d9659165ac6fcce1ac12e6c649d66dbdb9 @@ -19428,7 +19434,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0, is-core-module@npm:^2.5.0": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1, is-core-module@npm:^2.5.0": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -19637,6 +19643,13 @@ __metadata: languageName: node linkType: hard +"is-negative-zero@npm:^2.0.3": + version: 2.0.3 + resolution: "is-negative-zero@npm:2.0.3" + checksum: 10/8fe5cffd8d4fb2ec7b49d657e1691889778d037494c6f40f4d1a524cadd658b4b53ad7b6b73a59bcb4b143ae9a3d15829af864b2c0f9d65ac1e678c4c80f17e5 + languageName: node + linkType: hard + "is-node-process@npm:^1.0.1, is-node-process@npm:^1.2.0": version: 1.2.0 resolution: "is-node-process@npm:1.2.0" @@ -19800,7 +19813,7 @@ __metadata: languageName: node linkType: hard -"is-string@npm:^1.0.7, is-string@npm:^1.1.1": +"is-string@npm:^1.1.1": version: 1.1.1 resolution: "is-string@npm:1.1.1" dependencies: @@ -19890,12 +19903,12 @@ __metadata: languageName: node linkType: hard -"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0": - version: 1.1.0 - resolution: "is-weakref@npm:1.1.0" +"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.1": + version: 1.1.1 + resolution: "is-weakref@npm:1.1.1" dependencies: - call-bound: "npm:^1.0.2" - checksum: 10/89e627cc1763ea110574bb408fcf060ede47e70437d9278858bc939e3b3f7e4b7c558610b733da5f2ad6084d9f12b9c714b011ccf3fa771ec87e221c22bed910 + call-bound: "npm:^1.0.3" + checksum: 10/543506fd8259038b371bb083aac25b16cb4fd8b12fc58053aa3d45ac28dfd001cd5c6dffbba7aeea4213c74732d46b6cb2cfb5b412eed11f2db524f3f97d09a0 languageName: node linkType: hard @@ -23582,10 +23595,10 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.13.3": - version: 1.13.3 - resolution: "object-inspect@npm:1.13.3" - checksum: 10/14cb973d8381c69e14d7f1c8c75044eb4caf04c6dabcf40ca5c2ce42dc2073ae0bb2a9939eeca142b0c05215afaa1cd5534adb7c8879c32cba2576e045ed8368 +"object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10/aa13b1190ad3e366f6c83ad8a16ed37a19ed57d267385aa4bfdccda833d7b90465c057ff6c55d035a6b2e52c1a2295582b294217a0a3a1ae7abdd6877ef781fb languageName: node linkType: hard @@ -23695,7 +23708,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1": +"object.values@npm:^1.1.6, object.values@npm:^1.2.1": version: 1.2.1 resolution: "object.values@npm:1.2.1" dependencies: @@ -27202,7 +27215,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.3": +"regexp.prototype.flags@npm:^1.5.3, regexp.prototype.flags@npm:^1.5.4": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: @@ -29090,6 +29103,16 @@ __metadata: languageName: node linkType: hard +"stop-iteration-iterator@npm:^1.1.0": + version: 1.1.0 + resolution: "stop-iteration-iterator@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + internal-slot: "npm:^1.1.0" + checksum: 10/ff36c4db171ee76c936ccfe9541946b77017f12703d4c446652017356816862d3aa029a64e7d4c4ceb484e00ed4a81789333896390d808458638f3a216aa1f41 + languageName: node + linkType: hard + "storybook@npm:^8.6.2": version: 8.6.2 resolution: "storybook@npm:8.6.2" @@ -29279,7 +29302,7 @@ __metadata: languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9": +"string.prototype.trimend@npm:^1.0.9": version: 1.0.9 resolution: "string.prototype.trimend@npm:1.0.9" dependencies: @@ -31885,17 +31908,18 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": - version: 1.1.18 - resolution: "which-typed-array@npm:1.1.18" +"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19, which-typed-array@npm:^1.1.2": + version: 1.1.19 + resolution: "which-typed-array@npm:1.1.19" dependencies: available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - for-each: "npm:^0.3.3" + call-bound: "npm:^1.0.4" + for-each: "npm:^0.3.5" + get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-tostringtag: "npm:^1.0.2" - checksum: 10/11eed801b2bd08cdbaecb17aff381e0fb03526532f61acc06e6c7b9370e08062c33763a51f27825f13fdf34aabd0df6104007f4e8f96e6eaef7db0ce17a26d6e + checksum: 10/12be30fb88567f9863186bee1777f11bea09dd59ed8b3ce4afa7dd5cade75e2f4cc56191a2da165113cc7cf79987ba021dac1e22b5b62aa7e5c56949f2469a68 languageName: node linkType: hard From 0f9ea44144ba7d1fa364eb6864a095c9398ea31b Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 23 Jul 2025 09:55:58 -0500 Subject: [PATCH 023/101] XYChart: Lift restrictions around re-using fields for color and size (#106690) --- public/app/plugins/panel/xychart/SeriesEditor.tsx | 2 -- .../app/plugins/panel/xychart/XYChartTooltip.tsx | 14 +++++++------- public/app/plugins/panel/xychart/utils.ts | 10 ++-------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/public/app/plugins/panel/xychart/SeriesEditor.tsx b/public/app/plugins/panel/xychart/SeriesEditor.tsx index a68cf297ac3..54bc8085154 100644 --- a/public/app/plugins/panel/xychart/SeriesEditor.tsx +++ b/public/app/plugins/panel/xychart/SeriesEditor.tsx @@ -256,7 +256,6 @@ export const SeriesEditor = ({ settings: { // TODO: filter out series.y?.exclude.options, series.size.matcher.options, series.color.matcher.options filter: (field) => - field.name !== series.x?.matcher.options && (mapping === SeriesMapping.Auto || field.state?.origin?.frameIndex === series.frame?.matcher.options) && field.type === FieldType.number && @@ -292,7 +291,6 @@ export const SeriesEditor = ({ settings: { // TODO: filter out series.y?.exclude.options, series.size.matcher.options, series.color.matcher.options filter: (field) => - field.name !== series.x?.matcher.options && (mapping === SeriesMapping.Auto || field.state?.origin?.frameIndex === series.frame?.matcher.options) && field.type === FieldType.number && diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.tsx index 6760621fd82..585910e9184 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.tsx @@ -63,18 +63,18 @@ export const XYChartTooltip = ({ let label = series.name.value; - let seriesColor = series.color.fixed; - // let colorField = series.color.field; - // let pointColor: string; + let seriesColor = colorField?.display?.(colorField.values[rowIndex]).color ?? series.color.fixed ?? '#fff'; + let fillOpacity = colorField?.config.custom?.fillOpacity; - // if (colorField != null) { - // pointColor = colorField.display?.(colorField.values[rowIndex]).color!; - // } + // TODO: skip this if seriesColor already has an alpha component, such as opacity-by-value or opacity gradient schemes + if (fillOpacity != null) { + seriesColor = colorManipulator.alpha(seriesColor, fillOpacity / 100); + } const headerItem: VizTooltipItem = { label, value: '', - color: colorManipulator.alpha(seriesColor ?? '#fff', 0.5), + color: seriesColor, colorIndicator: ColorIndicator.marker_md, }; diff --git a/public/app/plugins/panel/xychart/utils.ts b/public/app/plugins/panel/xychart/utils.ts index df7fb0bc041..3526f7c9d6d 100644 --- a/public/app/plugins/panel/xychart/utils.ts +++ b/public/app/plugins/panel/xychart/utils.ts @@ -99,14 +99,8 @@ export function prepSeries( // only grabbing number fields (exclude time, string, enum, other) let onlyNumFields = onlyNumTimeFields.filter((field) => field.type === FieldType.number); - let color = - colorMatcher != null - ? onlyNumFields.find((field) => field !== x && colorMatcher!(field, frame, frames)) - : undefined; - let size = - sizeMatcher != null - ? onlyNumFields.find((field) => field !== x && field !== color && sizeMatcher!(field, frame, frames)) - : undefined; + let color = colorMatcher != null ? onlyNumFields.find((field) => colorMatcher(field, frame, frames)) : undefined; + let size = sizeMatcher != null ? onlyNumFields.find((field) => sizeMatcher(field, frame, frames)) : undefined; // x field is required if (x != null) { From 10b235ec755d6cd6564056bf58153711eddd6871 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Wed, 23 Jul 2025 17:10:22 +0200 Subject: [PATCH 024/101] New Logs Panel: always show horizontal scroll when unwrapped (#108530) --- public/app/features/logs/components/panel/LogList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index bbb5eca89c9..6350723ff89 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -475,7 +475,7 @@ const LogListComponent = ({ outerRef={scrollRef} overscanCount={5} ref={listRef} - style={{ overflowY: 'scroll' }} + style={wrapLogMessage ? { overflowY: 'scroll' } : { overflow: 'scroll' }} width="100%" > {Renderer} From a449a45452e8d775f2fae326cf290c2d9a0ae2e2 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Wed, 23 Jul 2025 10:40:55 -0500 Subject: [PATCH 025/101] Docs: What's new, Upgrade guide, and breaking changes v12.1 (#107277) Co-authored-by: Robby Milo --- .../upgrade-guide/upgrade-v12.1/index.md | 22 ++++++++ docs/sources/whatsnew/_index.md | 1 + docs/sources/whatsnew/whats-new-in-v12-1.md | 54 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 docs/sources/upgrade-guide/upgrade-v12.1/index.md create mode 100644 docs/sources/whatsnew/whats-new-in-v12-1.md diff --git a/docs/sources/upgrade-guide/upgrade-v12.1/index.md b/docs/sources/upgrade-guide/upgrade-v12.1/index.md new file mode 100644 index 00000000000..74095298c90 --- /dev/null +++ b/docs/sources/upgrade-guide/upgrade-v12.1/index.md @@ -0,0 +1,22 @@ +--- +description: Guide for upgrading to Grafana v12.1 +keywords: + - grafana + - configuration + - documentation + - upgrade + - '12.1' +title: Upgrade to Grafana v12.1 +menuTitle: Upgrade to v12.1 +weight: 499 +--- + +# Upgrade to Grafana v12.1 + +{{< docs/shared lookup="upgrade/intro_2.md" source="grafana" version="" >}} + +{{< docs/shared lookup="back-up/back-up-grafana.md" source="grafana" version="" leveloffset="+1" >}} + +{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="" >}} + +## Technical notes diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index 6c0a1386693..bcc9f3b4b4d 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -76,6 +76,7 @@ For a complete list of every change, with links to pull requests and related iss ## Grafana 12 +- [What's new in 12.1](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-1) - [What's new in 12.0](https://grafana.com/docs/grafana//whatsnew/whats-new-in-v12-0) ## Grafana 11 diff --git a/docs/sources/whatsnew/whats-new-in-v12-1.md b/docs/sources/whatsnew/whats-new-in-v12-1.md new file mode 100644 index 00000000000..93bd6fb093e --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v12-1.md @@ -0,0 +1,54 @@ +--- +description: Feature and improvement highlights for Grafana v12.1 +keywords: + - grafana + - new + - documentation + - '12.1' + - release notes +labels: + products: + - cloud + - enterprise + - oss +title: What's new in Grafana v12.1 +posts: + - title: Data sources + items: + - whats-new/2025-06-11-logicmonitor-enterprise-data-source-for-grafana.md + - whats-new/2025-06-20-support-for-service-account-impersonation-in-bigquery.md + - whats-new/2025-07-08-keep-instances-running-smoothly-with-grafana-advisor.md + - title: Alerting + items: + - whats-new/2025-07-11-alert-rule-list-page-updates.md + - whats-new/2025-07-09-active-time-intervals-in-grafana-alerting.md + - whats-new/2025-07-14-add-ability-to-import-rules-to-gma-from-prometheus-yaml.md + - title: Dashboards and visualizations + items: + - whats-new/2025-06-30-transformations-regression-analysis.md + - whats-new/2025-07-14-custom-variable-support-in-visualization-actions.md + - whats-new/2025-07-14-static-options-for-query-variable.md + - whats-new/2025-07-10-server-configurable-quick-time-ranges-for-dashboards.md + - whats-new/2025-07-14-enhanced-custom-currency-format-display-exact-financial-values.md + - title: Authentication and authorization + items: + - whats-new/2025-07-14-entra-id-workload-identity-support.md +whats_new_grafana_version: 12.1 +weight: -50 +--- + +# What’s new in Grafana v12.1 + +It’s 12.1 time! We’re really excited about GA for a new alert rule page that makes it easier to find what you need quickly, as well as the regression analysis transformation, helping you predict future data values or estimate missing data points that might not be exactly represented in the original dataset. Also, visualization actions now support custom variables. When triggered, actions prompt you to input whatever custom variable was defined, allowing you to tailor requests in real time without modifying dashboard configuration. This is super helpful for when you’re triggering alerts, filtering API calls, or sending user-defined parameters to external systems. And you can try out Grafana Advisor, which automatically detects plugin, data source, and SSO issues, keeping your Grafana instance healthy and secure. + +Speaking of security, Grafana now supports Entra Workload Identity, simplifying OAuth and increasing security for instances using Microsoft Azure. Thanks to community contributor [mehighlow](https://github.com/mehighlow) for this feature! + +We have one more community contributor to thank for this release. [Chris Hodges](https://github.com/chodges15) delivered server-configurable quick time ranges for dashboards. Now you can define custom time range presets for the time picker on dashboards, perfect for teams that routinely analyze specific, context-driven time windows. + +Keep reading to learn about what else we have in store for 12.1. + + + +For even more detail about all the changes in this release, refer to the [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). For the specific steps we recommend when you upgrade to v12.1, check out our [Upgrade Guide](https://grafana.com/docs/grafana//upgrade-guide/upgrade-v12.1/). + +{{< docs/whats-new >}} From e3c1e75da5f967b1411ac899050aaddca20aec13 Mon Sep 17 00:00:00 2001 From: Mihai Turdean <6640685+mihai-turdean@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:55:30 -0600 Subject: [PATCH 026/101] Fix Readme (#108081) --- pkg/services/authz/zanzana/schema/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/authz/zanzana/schema/README.md b/pkg/services/authz/zanzana/schema/README.md index 55425c151e7..f30d3ba7dea 100644 --- a/pkg/services/authz/zanzana/schema/README.md +++ b/pkg/services/authz/zanzana/schema/README.md @@ -15,7 +15,7 @@ them. This is because we want to store the folder tree relations. To grant a user access to a specific folder we store `{ “user”: “user:1”, relation: “read”, object:”folder:” }` -To grant a user access to sub resources of a folder we store ``{ “user”: “user:1”, relation: “resource_read”, object:”folder:”}` with additional context. +To grant a user access to sub resources of a folder we store `{ “user”: “user:1”, relation: “resource_read”, object:”folder:”}` with additional context. This context holds all GroupResources in a list e.g. `{ "subresources": ["dashboard.grafana.app/dashboards", "alerting.grafana.app/rules" ] }`. ## Resource level permissions From d3a445409be5ea437ece7efbf7486f1b7ba661e3 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Wed, 23 Jul 2025 18:12:12 +0200 Subject: [PATCH 027/101] shardQuerySplitting: group has pending requests or no shards to run (#108500) * shardQuerySplitting: group has pending requests or no shards to run * Comment * Regression test --- .../loki/shardQuerySplitting.test.ts | 22 ++++++++++++++++++- .../datasource/loki/shardQuerySplitting.ts | 3 ++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts index 04cb8c15055..12bb8b04de0 100644 --- a/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts +++ b/public/app/plugins/datasource/loki/shardQuerySplitting.test.ts @@ -6,7 +6,7 @@ import { LokiDatasource } from './datasource'; import { createLokiDatasource } from './mocks/datasource'; import { getMockFrames } from './mocks/frames'; import { runShardSplitQuery } from './shardQuerySplitting'; -import { LokiQuery, LokiQueryDirection } from './types'; +import { LokiQuery, LokiQueryDirection, LokiQueryType } from './types'; jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid'), @@ -100,6 +100,26 @@ describe('runShardSplitQuery()', () => { }); }); + test('Runs multiple non-sharded queries', async () => { + const request = createRequest([ + { expr: 'count_over_time({a="b"}[$__auto])', refId: 'A' }, + { expr: 'count_over_time({a="b"}[$__auto])', refId: 'B', queryType: LokiQueryType.Instant }, + ]); + datasource = createLokiDatasource({ + replace: (input = '') => { + return input.replace('$__auto', '5m').replace('$step', '5m'); + }, + getVariables: () => [], + }); + jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [] })); + datasource.languageProvider.fetchLabelValues = jest.fn(); + jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue([]); + await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => { + // 5 shards, 3 groups + empty shard group, 4 requests + expect(datasource.runQuery).toHaveBeenCalledTimes(2); + }); + }); + test('Users query splitting for querying over a day', async () => { await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => { // 5 shards, 3 groups + empty shard group, 4 requests diff --git a/public/app/plugins/datasource/loki/shardQuerySplitting.ts b/public/app/plugins/datasource/loki/shardQuerySplitting.ts index 6a949ece553..9cd5207da6d 100644 --- a/public/app/plugins/datasource/loki/shardQuerySplitting.ts +++ b/public/app/plugins/datasource/loki/shardQuerySplitting.ts @@ -87,8 +87,9 @@ function splitQueriesByStreamShard( } const nextRequest = () => { + // Find the next group to execute, which can be queries with pending shards to execute, or the next query with no shards. const nextGroup = - groups[group + 1] && groupHasPendingRequests(groups[group + 1]) + groups[group + 1] && (groups[group + 1].shards === undefined || groupHasPendingRequests(groups[group + 1])) ? groups[group + 1] : groups.find((shardGroup) => groupHasPendingRequests(shardGroup)); From cc869e7668c89818329efc47d68eea38d9751aae Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 23 Jul 2025 13:08:11 -0400 Subject: [PATCH 028/101] Alerting: Fix active time intervals when time interval is renamed (#108534) --- pkg/services/ngalert/provisioning/mute_timings.go | 6 ++++++ .../timeinterval/test-data/notification-settings.json | 3 +++ .../notifications/timeinterval/timeinterval_test.go | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/services/ngalert/provisioning/mute_timings.go b/pkg/services/ngalert/provisioning/mute_timings.go index 8ed7d93a597..6ad0f7efd65 100644 --- a/pkg/services/ngalert/provisioning/mute_timings.go +++ b/pkg/services/ngalert/provisioning/mute_timings.go @@ -440,6 +440,12 @@ func replaceMuteTiming(route *definitions.Route, oldName, newName string) int { updated++ } } + for idx := range route.ActiveTimeIntervals { + if route.ActiveTimeIntervals[idx] == oldName { + route.ActiveTimeIntervals[idx] = newName + updated++ + } + } for _, route := range route.Routes { updated += replaceMuteTiming(route, oldName, newName) } diff --git a/pkg/tests/apis/alerting/notifications/timeinterval/test-data/notification-settings.json b/pkg/tests/apis/alerting/notifications/timeinterval/test-data/notification-settings.json index 80f1b0438a0..33be4d06be5 100644 --- a/pkg/tests/apis/alerting/notifications/timeinterval/test-data/notification-settings.json +++ b/pkg/tests/apis/alerting/notifications/timeinterval/test-data/notification-settings.json @@ -15,6 +15,9 @@ ], "mute_time_intervals": [ "test-interval", "persisted-interval" + ], + "active_time_intervals": [ + "test-interval", "persisted-interval" ] } ] diff --git a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go index 8ea61ab6716..73656fcbc17 100644 --- a/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go +++ b/pkg/tests/apis/alerting/notifications/timeinterval/timeinterval_test.go @@ -725,7 +725,9 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) { updatedRoute := legacyCli.GetRoute(t) for idx, route := range updatedRoute.Routes { expectedTimeIntervals := replace(currentRoute.Routes[idx].MuteTimeIntervals, interval.Spec.Name, actual.Spec.Name) - assert.Equalf(t, expectedTimeIntervals, route.MuteTimeIntervals, "time interval in routes should have been renamed but it did not") + assert.Equalf(t, expectedTimeIntervals, route.MuteTimeIntervals, "mute time interval in routes should have been renamed but it did not") + expectedTimeIntervals = replace(currentRoute.Routes[idx].ActiveTimeIntervals, interval.Spec.Name, actual.Spec.Name) + assert.Equalf(t, expectedTimeIntervals, route.ActiveTimeIntervals, "active time interval in routes should have been renamed but it did not") } interval = *actual From 709aeb4e1aad3e50a3518f4d3d02d34882ee5230 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Wed, 23 Jul 2025 13:12:23 -0400 Subject: [PATCH 029/101] BulkDeleteProvisionedResource: Add unit tests (#108484) * BulkDeleteProvisionedResource: added unit tests and reorganize folder permission --------- Co-authored-by: Tom Ratcliffe --- .../BrowseActions/BrowseActions.tsx | 2 +- .../BulkActionFailureBanner.test.tsx | 151 +++++++++++++ .../BulkActionFailureBanner.tsx | 0 .../BulkActions/BulkActionProgress.test.tsx | 62 ++++++ .../{ => BulkActions}/BulkActionProgress.tsx | 4 +- .../BulkDeleteProvisionedResource.test.tsx | 201 ++++++++++++++++++ .../BulkDeleteProvisionedResource.tsx | 10 +- 7 files changed, 422 insertions(+), 8 deletions(-) create mode 100644 public/app/features/browse-dashboards/components/BulkActions/BulkActionFailureBanner.test.tsx rename public/app/features/browse-dashboards/components/{ => BulkActions}/BulkActionFailureBanner.tsx (100%) create mode 100644 public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx rename public/app/features/browse-dashboards/components/{ => BulkActions}/BulkActionProgress.tsx (89%) create mode 100644 public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx rename public/app/features/browse-dashboards/components/{ => BulkActions}/BulkDeleteProvisionedResource.tsx (97%) diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx index d246ccfcce9..85b50db08c0 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx @@ -14,7 +14,7 @@ import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDa import { useActionSelectionState } from '../../state/hooks'; import { setAllSelection } from '../../state/slice'; import { DashboardTreeSelection } from '../../types'; -import { BulkDeleteProvisionedResource } from '../BulkDeleteProvisionedResource'; +import { BulkDeleteProvisionedResource } from '../BulkActions/BulkDeleteProvisionedResource'; import { DeleteModal } from './DeleteModal'; import { MoveModal } from './MoveModal'; diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkActionFailureBanner.test.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionFailureBanner.test.tsx new file mode 100644 index 00000000000..9d91c74fbae --- /dev/null +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionFailureBanner.test.tsx @@ -0,0 +1,151 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { BulkActionFailureBanner, MoveResultFailed } from './BulkActionFailureBanner'; + +const setup = (resultOverrides?: Array>, onDismissOverride?: () => void) => { + const defaultFailedItems: MoveResultFailed[] = [ + { + status: 'failed', + title: 'Dashboard 1', + errorMessage: 'Permission denied', + }, + { + status: 'failed', + title: 'Dashboard 2', + errorMessage: 'Network error', + }, + ]; + + const result = + resultOverrides !== undefined + ? resultOverrides.map((override) => ({ status: 'failed' as const, title: 'Default Title', ...override })) + : defaultFailedItems; + + const onDismiss = onDismissOverride || jest.fn(); + + const props = { + result, + onDismiss, + }; + + return { + user: userEvent.setup(), + ...render(), + props, + }; +}; + +describe('BulkActionFailureBanner', () => { + it('should display error alert with correct item count', () => { + const testData = [{ title: 'Single Item', errorMessage: 'Single error' }]; + setup(testData); + + // Test that an alert is rendered + const alert = screen.getByRole('alert'); + expect(alert).toBeInTheDocument(); + + // Test structure: should have same number of list items as input data + const listItems = screen.getAllByRole('listitem'); + expect(listItems).toHaveLength(testData.length); + }); + + it('should render correct number of failed items with proper structure', () => { + const testData = [ + { title: 'Failed Dashboard A', errorMessage: 'Access denied' }, + { title: 'Failed Dashboard B', errorMessage: 'Validation failed' }, + ]; + setup(testData); + + const alert = screen.getByRole('alert'); + expect(alert).toBeInTheDocument(); + + // Test structure: number of list items matches input + const listItems = screen.getAllByRole('listitem'); + expect(listItems).toHaveLength(testData.length); + + // Test that each item has the expected structure (title + error message) + listItems.forEach((item, index) => { + const title = testData[index].title; + const errorMessage = testData[index].errorMessage; + + if (title) { + expect(item).toHaveTextContent(title); + } + if (errorMessage) { + expect(item).toHaveTextContent(errorMessage); + } + }); + }); + + it('should handle mixed scenarios', () => { + const testData = [ + { title: 'Item without error' }, + { title: 'Item with empty error', errorMessage: '' }, + { title: 'Another item with error', errorMessage: 'Another error' }, + ]; + setup(testData); + + const alert = screen.getByRole('alert'); + expect(alert).toBeInTheDocument(); + + const listItems = screen.getAllByRole('listitem'); + expect(listItems).toHaveLength(testData.length); + + // Test that items with error messages contain both title and error + // Items without errors should only contain title + testData.forEach((data, index) => { + const listItem = listItems[index]; + + // All items should have their title + if (data.title) { + expect(listItem).toHaveTextContent(data.title); + } + + // Only items with non-empty error messages should show the error + if (data.errorMessage && data.errorMessage.trim() !== '') { + expect(listItem).toHaveTextContent(data.errorMessage); + } + }); + }); + + it('should maintain list structure', () => { + const testData = [ + { title: 'Item 1', errorMessage: 'Error 1' }, + { title: 'Item 2', errorMessage: 'Error 2' }, + ]; + setup(testData); + + // Test semantic structure + const list = screen.getByRole('list'); + expect(list).toBeInTheDocument(); + + const listItems = screen.getAllByRole('listitem'); + expect(listItems).toHaveLength(testData.length); + }); + + it('should handle items without error messages', () => { + const testData = [{ title: 'Just Title Item' }]; + setup(testData); + + const listItems = screen.getAllByRole('listitem'); + expect(listItems).toHaveLength(1); + + const item = listItems[0]; + expect(item).toHaveTextContent(testData[0].title!); + // Should not contain colon separator when no error message + expect(item.textContent).not.toMatch(/:\s*.+$/); + }); + + it('should render dismissible alert', () => { + const onDismiss = jest.fn(); + setup([], onDismiss); + + const alert = screen.getByRole('alert'); + expect(alert).toBeInTheDocument(); + + // Alert should have close button (dismissible) + const closeButton = screen.getByRole('button', { name: /close/i }); + expect(closeButton).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/browse-dashboards/components/BulkActionFailureBanner.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionFailureBanner.tsx similarity index 100% rename from public/app/features/browse-dashboards/components/BulkActionFailureBanner.tsx rename to public/app/features/browse-dashboards/components/BulkActions/BulkActionFailureBanner.tsx diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx new file mode 100644 index 00000000000..e1dcbc41a94 --- /dev/null +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from '@testing-library/react'; + +import { BulkActionProgress, ProgressState } from './BulkActionProgress'; + +const setup = (progressOverrides: Partial = {}) => { + const defaultProgress: ProgressState = { + current: 5, + total: 10, + item: 'Test Dashboard', + ...progressOverrides, + }; + + const props = { + progress: defaultProgress, + }; + + return { + ...render(), + props, + }; +}; + +describe('BulkActionProgress', () => { + it('should render progress text with current and total values', () => { + setup({ current: 3, total: 8 }); + + expect(screen.getByText(/Progress: 3 of 8/)).toBeInTheDocument(); + }); + + it('should render current item being deleted', () => { + setup({ item: 'My Test Dashboard' }); + + expect(screen.getByText(/Deleting:/)).toBeInTheDocument(); + expect(screen.getByText(/My Test Dashboard/)).toBeInTheDocument(); + }); + + it('should handle edge case with total of 1', () => { + setup({ current: 1, total: 1 }); + + expect(screen.getByText(/Progress: 1 of 1/)).toBeInTheDocument(); + }); + + it('should handle edge case with zero current progress', () => { + setup({ current: 0, total: 5 }); + + expect(screen.getByText(/Progress: 0 of 5/)).toBeInTheDocument(); + }); + + it('should render all required elements together', () => { + setup({ current: 7, total: 15, item: 'Complex Dashboard Name' }); + + // Progress text + expect(screen.getByText(/Progress: 7 of 15/)).toBeInTheDocument(); + + // Spinner icon + expect(screen.getByTestId('Spinner')).toBeInTheDocument(); + + // Current item text + expect(screen.getByText(/Deleting:/)).toBeInTheDocument(); + expect(screen.getByText(/Complex Dashboard Name/)).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/browse-dashboards/components/BulkActionProgress.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx similarity index 89% rename from public/app/features/browse-dashboards/components/BulkActionProgress.tsx rename to public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx index 0b49fd547be..fd8ac5196a8 100644 --- a/public/app/features/browse-dashboards/components/BulkActionProgress.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx @@ -1,5 +1,5 @@ import { Trans } from '@grafana/i18n'; -import { Box, Icon, Text, Stack } from '@grafana/ui'; +import { Box, Text, Stack, Spinner } from '@grafana/ui'; import ProgressBar from 'app/features/provisioning/Shared/ProgressBar'; export interface ProgressState { @@ -21,7 +21,7 @@ export function BulkActionProgress({ progress }: { progress: ProgressState }) { values={{ current: progress.current, total: progress.total }} /> - + diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx new file mode 100644 index 00000000000..045efed7e54 --- /dev/null +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.test.tsx @@ -0,0 +1,201 @@ +import { screen, waitFor } from '@testing-library/react'; +import { HttpResponse, http } from 'msw'; +import { render } from 'test/test-utils'; + +import { setBackendSrv } from '@grafana/runtime'; +import server, { setupMockServer } from '@grafana/test-utils/server'; +import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; +import { backendSrv } from 'app/core/services/backend_srv'; + +import { BulkDeleteProvisionedResource } from './BulkDeleteProvisionedResource'; + +// Set up backendSrv as recommended in the PR comment +setBackendSrv(backendSrv); +setupMockServer(); + +jest.mock('../utils', () => ({ + collectSelectedItems: jest.fn().mockReturnValue([ + { uid: 'folder-1', isFolder: true, displayName: 'Test Folder' }, + { uid: 'dashboard-1', isFolder: false, displayName: 'Test Dashboard' }, + ]), + fetchProvisionedDashboardPath: jest.fn().mockResolvedValue('/test/dashboard.json'), +})); + +jest.mock('../../state/hooks', () => ({ + useChildrenByParentUIDState: jest.fn().mockReturnValue({}), + rootItemsSelector: jest.fn().mockReturnValue({ + items: [ + { uid: 'folder-1', title: 'Test Folder', kind: 'folder' }, + { uid: 'dashboard-1', title: 'Test Dashboard', kind: 'dashboard' }, + ], + }), +})); + +jest.mock('../../state/utils', () => ({ + findItem: jest.fn().mockImplementation((rootItems: unknown[], childrenByUID: unknown, uid: string) => { + const mockRootItems = [ + { uid: 'folder-1', title: 'Test Folder', kind: 'folder' }, + { uid: 'dashboard-1', title: 'Test Dashboard', kind: 'dashboard' }, + ]; + return mockRootItems.find((item) => item.uid === uid); + }), +})); + +jest.mock('../BrowseActions/DescendantCount', () => ({ + DescendantCount: jest.fn(({ selectedItems }) => ( +
+ Mocked descendant count for {Object.keys(selectedItems.folder).length} folders and{' '} + {Object.keys(selectedItems.dashboard).length} dashboards +
+ )), +})); + +jest.mock('app/features/provisioning/hooks/useGetResourceRepositoryView', () => ({ + useGetResourceRepositoryView: jest.fn(), +})); + +describe('BulkDeleteProvisionedResource', () => { + const defaultRepository: RepositoryView = { + name: 'test-folder', // This must match the folderUid passed to the component + type: 'github', + title: 'Test Repository', + target: 'folder', + workflows: ['branch', 'write'], + }; + + const selectedItems = { + folder: { 'folder-1': true }, + dashboard: { 'dashboard-1': true }, + }; + + beforeEach(() => { + server.use( + http.delete('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/:name/files/*', () => { + return HttpResponse.json({ + urls: { repositoryURL: 'https://github.com/test/repo' }, + }); + }) + ); + jest.clearAllMocks(); + + const { useGetResourceRepositoryView } = jest.requireMock( + 'app/features/provisioning/hooks/useGetResourceRepositoryView' + ); + useGetResourceRepositoryView.mockReturnValue({ + repository: defaultRepository, + folder: { + metadata: { + annotations: { + 'grafana.app/file-path': '/test/folder', + }, + }, + }, + isInstanceManaged: false, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function setup(repository: RepositoryView | null = defaultRepository) { + const onDismiss = jest.fn(); + + const { useGetResourceRepositoryView } = jest.requireMock( + 'app/features/provisioning/hooks/useGetResourceRepositoryView' + ); + useGetResourceRepositoryView.mockReturnValue({ + repository, + folder: repository + ? { + metadata: { + annotations: { + 'grafana.app/file-path': '/test/folder', + }, + }, + } + : null, + isInstanceManaged: false, + }); + + const renderResult = render( + + ); + + return { + onDismiss, + ...renderResult, + }; + } + + it('renders the delete warning and form', async () => { + setup(); + + expect(await screen.findByText(/This will delete selected folders and their descendants/)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Delete/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Cancel/i })).toBeInTheDocument(); + }); + + it('calls onDismiss when Cancel is clicked', async () => { + const { onDismiss, user } = setup(); + + await user.click(screen.getByRole('button', { name: /Cancel/i })); + + expect(onDismiss).toHaveBeenCalled(); + }); + + it('handles successful deletion', async () => { + const { user } = setup(); + + await user.click(screen.getByRole('button', { name: /Delete/i })); + + expect(await screen.findByText(/All resources have been deleted successfully/)).toBeInTheDocument(); + }); + + it('handles deletion errors', async () => { + const { user } = setup(); + + // Mock API to return error for this test + server.use( + http.delete('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/:name/files/*', () => { + return HttpResponse.json({ message: 'Network error' }, { status: 500 }); + }) + ); + + await user.click(screen.getByRole('button', { name: /Delete/i })); + + await waitFor(() => { + // Should show error alert with failed items + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByLabelText(/items failed/)).toBeInTheDocument(); + + // Should have error list items for both folder and dashboard + const errorItems = screen.getAllByRole('listitem'); + expect(errorItems).toHaveLength(2); // One for folder, one for dashboard + }); + }); + + it('shows loading state during deletion', async () => { + const { user } = setup(); + + // Mock slow API response + server.use( + http.delete('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/:name/files/*', async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + return HttpResponse.json({ + urls: { repositoryURL: 'https://github.com/test/repo' }, + }); + }) + ); + + await user.click(screen.getByRole('button', { name: /Delete/i })); + + expect(screen.getByText(/Deleting.../)).toBeInTheDocument(); + }); + + it('returns null when repository is not available', () => { + const { container } = setup(null); + + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/public/app/features/browse-dashboards/components/BulkDeleteProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx similarity index 97% rename from public/app/features/browse-dashboards/components/BulkDeleteProvisionedResource.tsx rename to public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx index 378e530672c..65e3463d60e 100644 --- a/public/app/features/browse-dashboards/components/BulkDeleteProvisionedResource.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx @@ -19,14 +19,14 @@ import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/us import { WorkflowOption } from 'app/features/provisioning/types'; import { useSelector } from 'app/types/store'; -import { useChildrenByParentUIDState, rootItemsSelector } from '../state/hooks'; -import { findItem } from '../state/utils'; -import { DashboardTreeSelection } from '../types'; +import { useChildrenByParentUIDState, rootItemsSelector } from '../../state/hooks'; +import { findItem } from '../../state/utils'; +import { DashboardTreeSelection } from '../../types'; +import { DescendantCount } from '../BrowseActions/DescendantCount'; +import { collectSelectedItems, fetchProvisionedDashboardPath } from '../utils'; -import { DescendantCount } from './BrowseActions/DescendantCount'; import { BulkActionFailureBanner, MoveResultFailed } from './BulkActionFailureBanner'; import { BulkActionProgress, ProgressState } from './BulkActionProgress'; -import { collectSelectedItems, fetchProvisionedDashboardPath } from './utils'; interface BulkDeleteFormData { comment: string; From 7c43c061a827e4802f40d74f451a15a24db6abd6 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Wed, 23 Jul 2025 20:16:06 +0200 Subject: [PATCH 030/101] Alerting: Fix deadlocks in provenance table (#106370) What is this feature? This PR further improves concurrent updates to the provenance table (a follow-up for #101688). The fix explicitly checks for a provenance record using SELECT ... FOR UPDATE for the databases that support it before performing either an update or upsert operation. This preemptive locking reduces the possibility of deadlocks in MySQL. This works only with the alertingProvenanceLockWrites feature flag enabled. Why do we need this feature? The current implementation (directly performing upserts without prior locking) may still encounter deadlocks because of the gap and insert-intention locks in some configurations, for example, with repeatable-read transaction isolation level in MySQL+InnoDB. --------- Co-authored-by: William Wernert --- .../src/types/featureToggles.gen.ts | 4 + pkg/services/featuremgmt/registry.go | 8 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 14 + .../ngalert/store/provisioning_store.go | 61 +++- .../ngalert/store/provisioning_store_test.go | 296 ++++++++++++------ 7 files changed, 277 insertions(+), 111 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 3b9875c801e..9582065043f 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -353,6 +353,10 @@ export interface FeatureToggles { */ alertmanagerRemoteSecondary?: boolean; /** + * Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL + */ + alertingProvenanceLockWrites?: boolean; + /** * Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. */ alertmanagerRemotePrimary?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index d91e76998e2..a5881618107 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -585,6 +585,14 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, }, + { + Name: "alertingProvenanceLockWrites", + Description: "Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromAdminPage: true, + HideFromDocs: true, + }, { Name: "alertmanagerRemotePrimary", Description: "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 689b0883024..8bf9cca2706 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -77,6 +77,7 @@ cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-e prometheusCodeModeMetricNamesSearch,experimental,@grafana/oss-big-tent,false,false,true addFieldFromCalculationStatFunctions,GA,@grafana/dataviz-squad,false,false,true alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false +alertingProvenanceLockWrites,experimental,@grafana/alerting-squad,false,false,false alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false extractFieldsNameDeduplication,experimental,@grafana/dataviz-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 5ea9f0f1995..5e8ccbb5615 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -319,6 +319,10 @@ const ( // Enable Grafana to sync configuration and state with a remote Alertmanager. FlagAlertmanagerRemoteSecondary = "alertmanagerRemoteSecondary" + // FlagAlertingProvenanceLockWrites + // Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL + FlagAlertingProvenanceLockWrites = "alertingProvenanceLockWrites" + // FlagAlertmanagerRemotePrimary // Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. FlagAlertmanagerRemotePrimary = "alertmanagerRemotePrimary" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2b3a1c27cf9..88ab6c38de0 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -351,6 +351,20 @@ "frontend": true } }, + { + "metadata": { + "name": "alertingProvenanceLockWrites", + "resourceVersion": "1753284360846", + "creationTimestamp": "2025-07-23T15:26:00Z" + }, + "spec": { + "description": "Enables a feature to avoid issues with concurrent writes to the alerting provenance table in MySQL", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "alertingQueryAndExpressionsStepMode", diff --git a/pkg/services/ngalert/store/provisioning_store.go b/pkg/services/ngalert/store/provisioning_store.go index d6999eec31d..f1306168134 100644 --- a/pkg/services/ngalert/store/provisioning_store.go +++ b/pkg/services/ngalert/store/provisioning_store.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/models" ) @@ -70,25 +71,61 @@ func (st DBstore) SetProvenance(ctx context.Context, o models.Provisionable, org // TODO: Add a unit-of-work pattern, so updating objects + provenance will happen consistently with rollbacks across stores. // TODO: Need to make sure that writing a record where our concurrency key fails will also fail the whole transaction. That way, this gets rolled back too. can't just check that 0 updates happened inmemory. Check with jp. If not possible, we need our own concurrency key. // TODO: Clean up stale provenance records periodically. - upsertSQL := st.SQLStore.GetDialect().UpsertSQL( - provenanceRecord{}.TableName(), - []string{"record_key", "record_type", "org_id"}, - []string{"record_key", "record_type", "org_id", "provenance"}) - params := []interface{}{ - recordKey, - recordType, - org, - p, + if st.FeatureToggles.IsEnabledGlobally(featuremgmt.FlagAlertingProvenanceLockWrites) { + return st.setProvenanceWithLocking(sess, recordKey, recordType, org, p) } + return st.setProvenanceUpsert(sess, recordKey, recordType, org, p) + }) +} - _, err := sess.SQL(upsertSQL, params...).Query() +func (st DBstore) setProvenanceUpsert(sess *db.Session, recordKey, recordType string, org int64, p models.Provenance) error { + upsertSQL := st.SQLStore.GetDialect().UpsertSQL( + provenanceRecord{}.TableName(), + []string{"record_key", "record_type", "org_id"}, + []string{"record_key", "record_type", "org_id", "provenance"}) + + params := []interface{}{ + recordKey, + recordType, + org, + p, + } + + _, err := sess.SQL(upsertSQL, params...).Query() + if err != nil { + return fmt.Errorf("failed to store provisioning status: %w", err) + } + return nil +} + +func (st DBstore) setProvenanceWithLocking(sess *db.Session, recordKey, recordType string, org int64, p models.Provenance) error { + // Check if the record exists with FOR UPDATE lock. + // If it does, we just update, otherwise we upsert the record. + // This is done to avoid deadlocks that can occur in MySQL when multiple transactions try to + // insert records (even different) because of the gap and insert intention locks. + exists, err := sess.Table(provenanceRecord{}). + Where("record_key = ? AND record_type = ? AND org_id = ?", recordKey, recordType, org). + ForUpdate(). + Exist() + if err != nil { + return fmt.Errorf("failed to check if provenance record exists: %w", err) + } + + if exists { + _, err = sess.Table(provenanceRecord{}). + Where("record_key = ? AND record_type = ? AND org_id = ?", recordKey, recordType, org). + Update(map[string]interface{}{ + "provenance": p, + }) if err != nil { return fmt.Errorf("failed to store provisioning status: %w", err) } - return nil - }) + } + + // Still upsert in case it was created while we were checking + return st.setProvenanceUpsert(sess, recordKey, recordType, org, p) } // DeleteProvenance deletes the provenance record from the table diff --git a/pkg/services/ngalert/store/provisioning_store_test.go b/pkg/services/ngalert/store/provisioning_store_test.go index 568f870027d..ea4daf9811b 100644 --- a/pkg/services/ngalert/store/provisioning_store_test.go +++ b/pkg/services/ngalert/store/provisioning_store_test.go @@ -2,10 +2,15 @@ package store_test import ( "context" + "fmt" + "math/rand" + "sync" "testing" + "time" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" @@ -19,113 +24,206 @@ func TestIntegrationProvisioningStore(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - store := createProvisioningStoreSut(tests.SetupTestEnv(t, testAlertingIntervalSeconds)) - t.Run("Default provenance of a known type is None", func(t *testing.T) { - rule := models.AlertRule{ - UID: "asdf", + testCases := []struct { + name string + featureEnabled bool + }{ + { + name: "without feature flag", + featureEnabled: false, + }, + { + name: "with feature flag", + featureEnabled: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ng, dbStore := tests.SetupTestEnv(t, testAlertingIntervalSeconds) + if tc.featureEnabled { + dbStore.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertingProvenanceLockWrites) + } + store := createProvisioningStoreSut(ng, dbStore) + + t.Run("Default provenance of a known type is None", func(t *testing.T) { + rule := models.AlertRule{ + UID: "asdf", + } + + provenance, err := store.GetProvenance(context.Background(), &rule, 1) + + require.NoError(t, err) + require.Equal(t, models.ProvenanceNone, provenance) + }) + + t.Run("Store returns saved provenance type", func(t *testing.T) { + rule := models.AlertRule{ + UID: "123", + } + err := store.SetProvenance(context.Background(), &rule, 1, models.ProvenanceFile) + require.NoError(t, err) + + p, err := store.GetProvenance(context.Background(), &rule, 1) + + require.NoError(t, err) + require.Equal(t, models.ProvenanceFile, p) + }) + + t.Run("Store does not get provenance of record with different org ID", func(t *testing.T) { + ruleOrg2 := models.AlertRule{ + UID: "456", + } + ruleOrg3 := models.AlertRule{ + UID: "456", + } + err := store.SetProvenance(context.Background(), &ruleOrg2, 2, models.ProvenanceFile) + require.NoError(t, err) + + p, err := store.GetProvenance(context.Background(), &ruleOrg3, 3) + + require.NoError(t, err) + require.Equal(t, models.ProvenanceNone, p) + }) + + t.Run("Store only updates provenance of record with given org ID", func(t *testing.T) { + ruleOrg2 := models.AlertRule{ + UID: "789", + OrgID: 2, + } + ruleOrg3 := models.AlertRule{ + UID: "789", + OrgID: 3, + } + err := store.SetProvenance(context.Background(), &ruleOrg2, 2, models.ProvenanceFile) + require.NoError(t, err) + err = store.SetProvenance(context.Background(), &ruleOrg3, 3, models.ProvenanceFile) + require.NoError(t, err) + + err = store.SetProvenance(context.Background(), &ruleOrg2, 2, models.ProvenanceAPI) + require.NoError(t, err) + + p, err := store.GetProvenance(context.Background(), &ruleOrg2, 2) + require.NoError(t, err) + require.Equal(t, models.ProvenanceAPI, p) + p, err = store.GetProvenance(context.Background(), &ruleOrg3, 3) + require.NoError(t, err) + require.Equal(t, models.ProvenanceFile, p) + }) + + t.Run("Store should return all provenances by type", func(t *testing.T) { + const orgID = 123 + rule1 := models.AlertRule{ + UID: "789", + OrgID: orgID, + } + rule2 := models.AlertRule{ + UID: "790", + OrgID: orgID, + } + err := store.SetProvenance(context.Background(), &rule1, orgID, models.ProvenanceFile) + require.NoError(t, err) + err = store.SetProvenance(context.Background(), &rule2, orgID, models.ProvenanceAPI) + require.NoError(t, err) + + p, err := store.GetProvenances(context.Background(), orgID, rule1.ResourceType()) + require.NoError(t, err) + require.Len(t, p, 2) + require.Equal(t, models.ProvenanceFile, p[rule1.UID]) + require.Equal(t, models.ProvenanceAPI, p[rule2.UID]) + }) + + t.Run("Store should delete provenance correctly", func(t *testing.T) { + const orgID = 1234 + ruleOrg := models.AlertRule{ + UID: "7834539", + OrgID: orgID, + } + err := store.SetProvenance(context.Background(), &ruleOrg, orgID, models.ProvenanceFile) + require.NoError(t, err) + p, err := store.GetProvenance(context.Background(), &ruleOrg, orgID) + require.NoError(t, err) + require.Equal(t, models.ProvenanceFile, p) + + err = store.DeleteProvenance(context.Background(), &ruleOrg, orgID) + require.NoError(t, err) + + p, err = store.GetProvenance(context.Background(), &ruleOrg, orgID) + require.NoError(t, err) + require.Equal(t, models.ProvenanceNone, p) + }) + }) + } +} + +func TestSetProvenance_DeadlockScenarios(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ng, dbStore := tests.SetupTestEnv(t, testAlertingIntervalSeconds) + dbStore.FeatureToggles = featuremgmt.WithFeatures(featuremgmt.FlagAlertingProvenanceLockWrites) + store := createProvisioningStoreSut(ng, dbStore) + concurrency := 20 + + t.Run("Same record, different orgs", func(t *testing.T) { + rule := &models.AlertRule{UID: "same-record-diff-orgs"} + + var wg sync.WaitGroup + for i := range concurrency { + wg.Add(1) + go func(orgID int64) { + defer wg.Done() + time.Sleep(time.Microsecond * time.Duration(rand.Intn(100))) + err := store.SetProvenance(context.Background(), rule, orgID, models.ProvenanceAPI) + require.NoError(t, err) + }(int64(i + 1)) } - - provenance, err := store.GetProvenance(context.Background(), &rule, 1) - - require.NoError(t, err) - require.Equal(t, models.ProvenanceNone, provenance) + wg.Wait() }) - t.Run("Store returns saved provenance type", func(t *testing.T) { - rule := models.AlertRule{ - UID: "123", + t.Run("Different records, same org", func(t *testing.T) { + orgID := int64(1) + + var wg sync.WaitGroup + for i := range concurrency { + wg.Add(1) + go func(uid string) { + defer wg.Done() + time.Sleep(time.Microsecond * time.Duration(rand.Intn(100))) + rule := &models.AlertRule{UID: uid} + err := store.SetProvenance(context.Background(), rule, orgID, models.ProvenanceFile) + require.NoError(t, err) + }(fmt.Sprintf("diff-record-same-org-%d", i)) } - err := store.SetProvenance(context.Background(), &rule, 1, models.ProvenanceFile) - require.NoError(t, err) - - p, err := store.GetProvenance(context.Background(), &rule, 1) - - require.NoError(t, err) - require.Equal(t, models.ProvenanceFile, p) + wg.Wait() }) - t.Run("Store does not get provenance of record with different org ID", func(t *testing.T) { - ruleOrg2 := models.AlertRule{ - UID: "456", + t.Run("Mixed operations", func(t *testing.T) { + rule := &models.AlertRule{UID: "mixed-ops"} + orgID := int64(1) + + var wg sync.WaitGroup + // Mix SetProvenance and GetProvenance operations + for range concurrency { + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(time.Microsecond * time.Duration(rand.Intn(100))) + err := store.SetProvenance(context.Background(), rule, orgID, models.ProvenanceAPI) + require.NoError(t, err) + }() + + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(time.Microsecond * time.Duration(rand.Intn(100))) + _, err := store.GetProvenance(context.Background(), rule, orgID) + require.NoError(t, err) + }() } - ruleOrg3 := models.AlertRule{ - UID: "456", - } - err := store.SetProvenance(context.Background(), &ruleOrg2, 2, models.ProvenanceFile) - require.NoError(t, err) - - p, err := store.GetProvenance(context.Background(), &ruleOrg3, 3) - - require.NoError(t, err) - require.Equal(t, models.ProvenanceNone, p) - }) - - t.Run("Store only updates provenance of record with given org ID", func(t *testing.T) { - ruleOrg2 := models.AlertRule{ - UID: "789", - OrgID: 2, - } - ruleOrg3 := models.AlertRule{ - UID: "789", - OrgID: 3, - } - err := store.SetProvenance(context.Background(), &ruleOrg2, 2, models.ProvenanceFile) - require.NoError(t, err) - err = store.SetProvenance(context.Background(), &ruleOrg3, 3, models.ProvenanceFile) - require.NoError(t, err) - - err = store.SetProvenance(context.Background(), &ruleOrg2, 2, models.ProvenanceAPI) - require.NoError(t, err) - - p, err := store.GetProvenance(context.Background(), &ruleOrg2, 2) - require.NoError(t, err) - require.Equal(t, models.ProvenanceAPI, p) - p, err = store.GetProvenance(context.Background(), &ruleOrg3, 3) - require.NoError(t, err) - require.Equal(t, models.ProvenanceFile, p) - }) - - t.Run("Store should return all provenances by type", func(t *testing.T) { - const orgID = 123 - rule1 := models.AlertRule{ - UID: "789", - OrgID: orgID, - } - rule2 := models.AlertRule{ - UID: "790", - OrgID: orgID, - } - err := store.SetProvenance(context.Background(), &rule1, orgID, models.ProvenanceFile) - require.NoError(t, err) - err = store.SetProvenance(context.Background(), &rule2, orgID, models.ProvenanceAPI) - require.NoError(t, err) - - p, err := store.GetProvenances(context.Background(), orgID, rule1.ResourceType()) - require.NoError(t, err) - require.Len(t, p, 2) - require.Equal(t, models.ProvenanceFile, p[rule1.UID]) - require.Equal(t, models.ProvenanceAPI, p[rule2.UID]) - }) - - t.Run("Store should delete provenance correctly", func(t *testing.T) { - const orgID = 1234 - ruleOrg := models.AlertRule{ - UID: "7834539", - OrgID: orgID, - } - err := store.SetProvenance(context.Background(), &ruleOrg, orgID, models.ProvenanceFile) - require.NoError(t, err) - p, err := store.GetProvenance(context.Background(), &ruleOrg, orgID) - require.NoError(t, err) - require.Equal(t, models.ProvenanceFile, p) - - err = store.DeleteProvenance(context.Background(), &ruleOrg, orgID) - require.NoError(t, err) - - p, err = store.GetProvenance(context.Background(), &ruleOrg, orgID) - require.NoError(t, err) - require.Equal(t, models.ProvenanceNone, p) + wg.Wait() }) } From 60e0d2d136cd8b90a8c5f4a5bf52b40bdf42f5e7 Mon Sep 17 00:00:00 2001 From: Tania <10127682+undef1nd@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:27:21 +0200 Subject: [PATCH 031/101] OpenFeature: Update authenticated request check (#108539) --- pkg/registry/apis/ofrep/register.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index de3f5ab2702..684e1f402e1 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -319,7 +319,7 @@ func (b *APIBuilder) isAuthenticatedRequest(r *http.Request) bool { if !ok { return false } - return user.GetIdentityType() != "" + return user.GetIdentityType() != types.TypeUnauthenticated } // validateNamespace checks if the namespace in the evaluation context matches the namespace in the request From d9ef08f8248367a7aa05f5c7d2730673044c0b55 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 23 Jul 2025 14:49:10 -0400 Subject: [PATCH 032/101] TableNG: Footer fields declaration should work for displayName or name (#108482) back to using our own getDisplayNam --- .../src/components/Table/TableNG/TableNG.tsx | 2 +- .../components/Table/TableNG/hooks.test.ts | 124 +++++++++++------- .../src/components/Table/TableNG/hooks.ts | 26 +++- .../src/components/Table/TableNG/utils.ts | 5 +- 4 files changed, 103 insertions(+), 54 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index edcb619702e..c23f3a7384b 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -208,7 +208,7 @@ export function TableNG(props: TableNGProps) { }); // Create a map of column key to text wrap - const footerCalcs = useFooterCalcs(sortedRows, data.fields, { enabled: hasFooter, footerOptions, isCountRowsSet }); + const footerCalcs = useFooterCalcs(sortedRows, data, { enabled: hasFooter, footerOptions, isCountRowsSet }); const applyToRowBgFn = useMemo(() => getApplyToRowBgFn(data.fields, theme) ?? undefined, [data.fields, theme]); const renderRow = useMemo( diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts index 094804a3a28..afbc4fff691 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react'; import { varPreLine } from 'uwrap'; -import { Field, FieldType } from '@grafana/data'; +import { cacheFieldDisplayNames, createDataFrame, Field, FieldType } from '@grafana/data'; import { useFilteredRows, @@ -226,11 +226,11 @@ describe('TableNG hooks', () => { describe('useFooterCalcs', () => { const rows = [ - { Field1: 1, Text: 'a', __depth: 0, __index: 0 }, - { Field1: 2, Text: 'b', __depth: 0, __index: 1 }, - { Field1: 3, Text: 'c', __depth: 0, __index: 2 }, - { Field2: 3, Text: 'd', __depth: 0, __index: 3 }, - { Field2: 10, Text: 'e', __depth: 0, __index: 4 }, + { 'Field 1': 1, Text: 'a', __depth: 0, __index: 0 }, + { 'Field 1': 2, Text: 'b', __depth: 0, __index: 1 }, + { 'Field 1': 3, Text: 'c', __depth: 0, __index: 2 }, + { 'Field 2': 3, Text: 'd', __depth: 0, __index: 3 }, + { 'Field 2': 10, Text: 'e', __depth: 0, __index: 4 }, ]; const numericField: Field = { @@ -239,6 +239,7 @@ describe('TableNG hooks', () => { values: [1, 2, 3], config: { custom: {}, + displayName: 'Field 1', }, display: (value: unknown) => ({ text: String(value), @@ -247,7 +248,6 @@ describe('TableNG hooks', () => { prefix: undefined, suffix: undefined, }), - state: {}, getLinks: undefined, }; @@ -255,7 +255,10 @@ describe('TableNG hooks', () => { name: 'Field2', type: FieldType.number, values: [3, 10], - config: { custom: {} }, + config: { + custom: {}, + displayName: 'Field 2', + }, display: (value: unknown) => ({ text: String(value), numeric: Number(value), @@ -263,7 +266,6 @@ describe('TableNG hooks', () => { prefix: undefined, suffix: undefined, }), - state: {}, getLinks: undefined, }; @@ -279,105 +281,135 @@ describe('TableNG hooks', () => { prefix: undefined, suffix: undefined, }), - state: {}, getLinks: undefined, }; it('should calculate sum for numeric fields', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, numericField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['sum'] }, - }) - ); + }); + }); expect(result.current).toEqual(['Total', '6']); // 1 + 2 + 3 }); it('should calculate mean for numeric fields', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, numericField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['mean'] }, - }) - ); + }); + }); expect(result.current).toEqual(['Mean', '2']); // (1 + 2 + 3) / 3 }); it('should return an empty string for non-numeric fields', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, textField] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['sum'] }, - }) - ); + }); + }); expect(result.current).toEqual(['Total', '']); }); it('should return empty array if no footerOptions are provided', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: undefined, - }) - ); + }); + }); expect(result.current).toEqual([]); }); it('should return empty array when footer is disabled', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: false, footerOptions: { show: true, reducer: ['sum'] }, - }) - ); + }); + }); expect(result.current).toEqual([]); }); it('should return empty array when reducer is undefined', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, textField] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: undefined }, - }) - ); + }); + }); expect(result.current).toEqual([]); }); it('should return empty array when reducer is empty', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, textField], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: [] }, - }) - ); + }); + }); expect(result.current).toEqual([]); }); it('should return empty string if fields array doesnt include this field', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, numericField, numericField2], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['sum'], fields: ['Field2', 'Field3'] }, - }) - ); + }); + }); expect(result.current).toEqual(['Total', '', '13']); }); it('should return the calculation if fields array includes this field', () => { - const { result } = renderHook(() => - useFooterCalcs(rows, [textField, numericField, numericField2], { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { enabled: true, footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field2', 'Field3'] }, - }) - ); + }); + }); + + expect(result.current).toEqual(['Total', '6', '13']); + }); + + it('should return the calculation if fields array includes this field by either name or display name', () => { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [textField, numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data, { + enabled: true, + footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field 2'] }, + }); + }); expect(result.current).toEqual(['Total', '6', '13']); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts index 7e0229903fb..0533c3cf3d0 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts @@ -2,7 +2,16 @@ import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect, Ref import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid'; import { varPreLine } from 'uwrap'; -import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data'; +import { + DataFrame, + Field, + FieldMatcherID, + fieldReducers, + FieldType, + formattedValueToString, + getFieldMatcher, + reduceField, +} from '@grafana/data'; import { useTheme2 } from '../../../themes/ThemeContext'; import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types'; @@ -255,7 +264,7 @@ export interface FooterCalcsOptions { export function useFooterCalcs( rows: TableRow[], - fields: Field[], + data: DataFrame, { enabled, footerOptions, isCountRowsSet }: FooterCalcsOptions ): string[] { return useMemo(() => { @@ -265,7 +274,11 @@ export function useFooterCalcs( return []; } - return fields.map((field, index) => { + const fieldNameMatcher = footerOptions.fields + ? getFieldMatcher({ id: FieldMatcherID.byNames, options: { names: footerOptions.fields } }) + : undefined; + + return data.fields.map((field, index) => { if (field.state?.calcs) { delete field.state?.calcs; } @@ -289,8 +302,9 @@ export function useFooterCalcs( return ''; } - // If fields array is specified, only show footer for fields included in that array - if (footerOptions.fields?.length && !footerOptions.fields?.includes(getDisplayName(field))) { + // If fields array is specified, only show footer for fields included in that array. + // the array can include either the display name or the field name. + if (fieldNameMatcher && !fieldNameMatcher(field, data, [data])) { return ''; } @@ -305,7 +319,7 @@ export function useFooterCalcs( return formattedValueToString(displayFn(value)); }); - }, [fields, enabled, footerOptions, isCountRowsSet, rows]); + }, [data, enabled, footerOptions, isCountRowsSet, rows]); } interface TypographyCtx { diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index 191e9fa76e4..e0b83527470 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -537,7 +537,10 @@ export const processNestedTableRows = ( /** * @internal - * returns the display name of a field + * returns the display name of a field. + * We intentionally do not want to use @grafana/data's getFieldDisplayName here, + * instead we have a call to cacheFieldDisplayNames up in TablePanel to handle this + * before we begin. */ export const getDisplayName = (field: Field): string => { return field.state?.displayName ?? field.name; From 26d26f67d49842c5337e7848613e079a1a6a9c64 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 23 Jul 2025 15:39:25 -0400 Subject: [PATCH 033/101] TableNG: Integrate hover and selection, DataLinkCell cleanup (#108353) * Table: Style cleanups (minus DataLinkCell word wrap) * kill JSONCell in favor of a custom display method and style overrides at TableNG * remove unused type for JSONCellProps * add increased specificity to CSS selector * remove inherit and rely on undefined * fix tests * shrink and optimize DataLinkCell * maybe * format files * better * classname * add Pills and DataLink cells to kitchen sink * add comment about align + justify, simplify datalinks targeting * simplify? * poke * tweak * revert * fix one more z-index conflict * clean up alignment tests * a couple more tests * make TableNG e2e tests more resilient to changes to the gdev dashboard --------- Co-authored-by: Leon Sorokin --- .../panel-table/table_kitchen_sink.json | 83 +++++++- .../panels-suite/table-kitchenSink.spec.ts | 92 +++++++-- .../Table/TableNG/Cells/DataLinksCell.tsx | 44 +--- .../Table/TableNG/Cells/JSONCell.tsx | 49 ----- .../Table/TableNG/Cells/SparklineCell.tsx | 9 +- .../Table/TableNG/Cells/renderers.test.tsx | 9 + .../Table/TableNG/Cells/renderers.tsx | 12 +- .../src/components/Table/TableNG/TableNG.tsx | 81 ++++++-- .../src/components/Table/TableNG/hooks.ts | 2 - .../src/components/Table/TableNG/types.ts | 7 - .../components/Table/TableNG/utils.test.ts | 191 ++++++++---------- .../src/components/Table/TableNG/utils.ts | 103 ++++++---- public/locales/en-US/grafana.json | 5 +- 13 files changed, 389 insertions(+), 298 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx diff --git a/devenv/dev-dashboards/panel-table/table_kitchen_sink.json b/devenv/dev-dashboards/panel-table/table_kitchen_sink.json index 6123deb638d..c92924966ce 100644 --- a/devenv/dev-dashboards/panel-table/table_kitchen_sink.json +++ b/devenv/dev-dashboards/panel-table/table_kitchen_sink.json @@ -79,10 +79,6 @@ { "id": "max", "value": 100 - }, - { - "id": "custom.width", - "value": 300 } ] }, @@ -280,13 +276,75 @@ "type": "auto", "wrapText": true } + }, + { + "id": "custom.width", + "value": 300 } ] + }, + { + "matcher": { + "id": "byName", + "options": "Pills" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "pill" + } + }, + { + "id": "custom.width", + "value": 120 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Data Link" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "data-links" + } + }, + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "Product", + "url": "${__value.text}" + }, + { + "targetBlank": true, + "title": "Grafana", + "url": "https://grafana.com" + } + ] + }, + { + "id": "custom.width", + "value": 180 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Gauge" + }, + "properties": [] } ] }, "gridPos": { - "h": 18, + "h": 15, "w": 24, "x": 0, "y": 0 @@ -297,7 +355,10 @@ "footer": { "countRows": false, "enablePagination": false, - "fields": "", + "fields": [ + "Min", + "Max" + ], "reducer": [ "max" ], @@ -318,7 +379,7 @@ "scenarioId": "random_walk_table" }, { - "csvContent": "Info,Image,Long Text\ndown,https://placecats.com/millie/300/300,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse tempus et augue et lacinia. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec eu pretium tortor. Cras venenatis sapien sed mauris gravida, ut scelerisque est fringilla. Cras lorem diam, facilisis nec malesuada in, vulputate vel enim. Etiam fringilla nisi quis felis blandit tincidunt. Cras id lacus ornare, ullamcorper nisl eget, bibendum odio. Pellentesque imperdiet, leo a imperdiet venenatis, ligula risus venenatis quam, vel euismod magna nisi sit amet leo.\"\nup,https://placecats.com/neo/300/300,\"Sed imperdiet eget diam sit amet fringilla. Curabitur quis lacus blandit, mollis diam non, accumsan tortor. Aliquam ac tellus eget dui facilisis tempor eu id nulla. Maecenas ultrices turpis eu elementum imperdiet. Fusce eget rhoncus mi, et egestas lectus. Mauris facilisis auctor enim sed malesuada. Maecenas placerat ultricies metus vitae viverra. In hac habitasse platea dictumst. Mauris ipsum nisl, dictum eu aliquam eleifend, rutrum id orci. Nullam eget dui et odio eleifend porttitor.\"\nup fast,https://placecats.com/bella/300/300,\"Proin ac libero vulputate ex vulputate pharetra ut vel lacus. Phasellus quis dolor sed leo finibus scelerisque. Ut vel finibus leo, sed viverra ipsum. Suspendisse vitae rutrum arcu. Donec sed tellus vel lectus bibendum vestibulum. Sed eu felis non velit dictum pulvinar eu et leo. Aenean et dignissim arcu. Nam luctus at neque quis efficitur. Fusce tempus at nibh a imperdiet. Nullam malesuada ac magna at facilisis. Duis pretium aliquam eros. Donec pharetra dignissim dolor non bibendum. Ut gravida mi id urna tempus, at ullamcorper felis vulputate. Duis congue augue ex, sed finibus leo ornare ut. Mauris non quam sodales, dignissim lorem eget, tincidunt mauris. Aliquam ut velit auctor, vestibulum metus sed, mollis massa.\"\ndown fast,https://placecats.com/neo_2/300/300,\"Nullam in pulvinar justo. Nunc dictum arcu ac pellentesque bibendum. Sed in erat turpis. Vestibulum eu orci ac ligula lobortis tempus. Fusce consectetur feugiat magna, eu tempor nibh vestibulum ac. Aliquam erat volutpat. Vivamus sit amet viverra enim. Quisque mollis odio nulla, nec vulputate sem placerat in. Etiam dolor sapien, pulvinar in accumsan at, consequat eget nisi. Nunc condimentum neque magna, congue consectetur dui efficitur interdum. Nam lobortis fringilla maximus. Vestibulum eu dui a velit condimentum eleifend consequat nec lectus.\"", + "csvContent": "Info,Image,Pills,Data Link,Long Text\ndown,https://placecats.com/millie/200/400,hello,https://grafana.com,\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse tempus et augue et lacinia. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec eu pretium tortor. Cras venenatis sapien sed mauris gravida, ut scelerisque est fringilla. Cras lorem diam, facilisis nec malesuada in, vulputate vel enim. Etiam fringilla nisi quis felis blandit tincidunt. Cras id lacus ornare, ullamcorper nisl eget, bibendum odio. Pellentesque imperdiet, leo a imperdiet venenatis, ligula risus venenatis quam, vel euismod magna nisi sit amet leo.\"\nup,https://placecats.com/neo/200/400,\"[1,2,3,\"\"foo\"\",\"\"bar\"\"]\",https://grafana.com/solutions/kubernetes/,\"Sed imperdiet eget diam sit amet fringilla. Curabitur quis lacus blandit, mollis diam non, accumsan tortor. Aliquam ac tellus eget dui facilisis tempor eu id nulla. Maecenas ultrices turpis eu elementum imperdiet. Fusce eget rhoncus mi, et egestas lectus. Mauris facilisis auctor enim sed malesuada. Maecenas placerat ultricies metus vitae viverra. In hac habitasse platea dictumst. Mauris ipsum nisl, dictum eu aliquam eleifend, rutrum id orci. Nullam eget dui et odio eleifend porttitor.\"\nup fast,https://placecats.com/bella/200/400,\"foo,1,4,beep\",https://k6.io/,\"Proin ac libero vulputate ex vulputate pharetra ut vel lacus. Phasellus quis dolor sed leo finibus scelerisque. Ut vel finibus leo, sed viverra ipsum. Suspendisse vitae rutrum arcu. Donec sed tellus vel lectus bibendum vestibulum. Sed eu felis non velit dictum pulvinar eu et leo. Aenean et dignissim arcu. Nam luctus at neque quis efficitur. Fusce tempus at nibh a imperdiet. Nullam malesuada ac magna at facilisis. Duis pretium aliquam eros. Donec pharetra dignissim dolor non bibendum. Ut gravida mi id urna tempus, at ullamcorper felis vulputate. Duis congue augue ex, sed finibus leo ornare ut. Mauris non quam sodales, dignissim lorem eget, tincidunt mauris. Aliquam ut velit auctor, vestibulum metus sed, mollis massa.\"\ndown fast,https://placecats.com/neo_2/200/400,\"foo,bar,baz,a longer one,bim\",https://grafana.com/products/cloud/,\"Nullam in pulvinar justo. Nunc dictum arcu ac pellentesque bibendum. Sed in erat turpis. Vestibulum eu orci ac ligula lobortis tempus. Fusce consectetur feugiat magna, eu tempor nibh vestibulum ac. Aliquam erat volutpat. Vivamus sit amet viverra enim. Quisque mollis odio nulla, nec vulputate sem placerat in. Etiam dolor sapien, pulvinar in accumsan at, consequat eget nisi. Nunc condimentum neque magna, congue consectetur dui efficitur interdum. Nam lobortis fringilla maximus. Vestibulum eu dui a velit condimentum eleifend consequat nec lectus.\"", "datasource": { "type": "grafana-testdata-datasource", "uid": "PD8C576611E62080A" @@ -344,12 +405,14 @@ }, "includeByName": {}, "indexByName": { - "A": 7, + "A": 9, + "Data Link": 8, "Image": 5, "Info": 1, - "Long Text": 6, + "Long Text": 7, "Max A": 3, "Min A": 2, + "Pills": 6, "State A": 4, "Time A": 0 }, @@ -461,5 +524,5 @@ "timezone": "", "title": "Panel Tests - Table - Kitchen Sink", "uid": "dcb9f5e9-8066-4397-889e-864b99555dbb", - "version": 1 + "version": 2 } diff --git a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts index 29fea517fae..7052ccfc510 100644 --- a/e2e-playwright/panels-suite/table-kitchenSink.spec.ts +++ b/e2e-playwright/panels-suite/table-kitchenSink.spec.ts @@ -2,14 +2,20 @@ import { Page, Locator } from '@playwright/test'; import { test, expect } from '@grafana/plugin-e2e'; +const DASHBOARD_UID = 'dcb9f5e9-8066-4397-889e-864b99555dbb'; + test.use({ - viewport: { width: 1600, height: 1080 }, + viewport: { width: 2000, height: 1080 }, featureToggles: { tableNextGen: true, }, }); // helper utils +const waitForTableLoad = async (loc: Page | Locator) => { + await expect(loc.locator('.rdg')).toBeVisible(); +}; + const getCell = async (loc: Page | Locator, rowIdx: number, colIdx: number) => loc .getByRole('row') @@ -22,6 +28,24 @@ const getCellHeight = async (loc: Page | Locator, rowIdx: number, colIdx: number return (await cell.boundingBox())?.height ?? 0; }; +const getColumnIdx = async (loc: Page | Locator, columnName: string) => { + // find the index of the column "Long text." The kitchen sink table will change over time, but + // we can just find the column programatically and use it throughout the test. + let result = -1; + const colCount = await loc.getByRole('columnheader').count(); + for (let colIdx = 0; colIdx < colCount; colIdx++) { + const cell = await getCell(loc, 0, colIdx); + if ((await cell.textContent()) === columnName) { + result = colIdx; + break; + } + } + if (result === -1) { + throw new Error(`Could not find the "${columnName}" column in the table`); + } + return result; +}; + test.describe( 'Panels test: Table - Kitchen Sink', { @@ -30,7 +54,7 @@ test.describe( () => { test('Tests word wrap, hover overflow, and cell inspect', async ({ gotoDashboardPage, selectors, page }) => { const dashboardPage = await gotoDashboardPage({ - uid: 'dcb9f5e9-8066-4397-889e-864b99555dbb', + uid: DASHBOARD_UID, queryParams: new URLSearchParams({ editPanel: '1' }), }); @@ -38,22 +62,28 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')) ).toBeVisible(); + // to avoid a race condition when counting up , wait for react-data-grid to finish rendering. + await waitForTableLoad(page); + + const longTextColIdx = await getColumnIdx(page, 'Long Text'); + // text wrapping is enabled by default on this panel. - await expect(getCellHeight(page, 1, 5)).resolves.toBeGreaterThan(100); + await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100); // toggle the lorem ipsum column's wrap text toggle and confirm that the height shrinks. await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Wrap text')) .last() .click(); - await expect(getCellHeight(page, 1, 5)).resolves.toBeLessThan(100); + await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100); // test that hover overflow works. - const loremIpsumCell = await getCell(page, 1, 5); + const loremIpsumCell = await getCell(page, 1, longTextColIdx); + await loremIpsumCell.scrollIntoViewIfNeeded(); await loremIpsumCell.hover(); - await expect(getCellHeight(page, 1, 5)).resolves.toBeGreaterThan(100); - await (await getCell(page, 1, 6)).hover(); - await expect(getCellHeight(page, 1, 5)).resolves.toBeLessThan(100); + await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeGreaterThan(100); + await (await getCell(page, 1, longTextColIdx + 1)).hover(); + await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100); // enable cell inspect, confirm that hover no longer triggers. await dashboardPage @@ -64,7 +94,7 @@ test.describe( .locator('label[for="custom.inspect"]') .click(); await loremIpsumCell.hover(); - await expect(getCellHeight(page, 1, 5)).resolves.toBeLessThan(100); + await expect(getCellHeight(page, 1, longTextColIdx)).resolves.toBeLessThan(100); // click cell inspect, check that cell inspection pops open in the side as we'd expect. await loremIpsumCell.getByLabel('Inspect value').click(); @@ -75,7 +105,7 @@ test.describe( test('Tests visibility and display name via overrides', async ({ gotoDashboardPage, selectors, page }) => { const dashboardPage = await gotoDashboardPage({ - uid: 'dcb9f5e9-8066-4397-889e-864b99555dbb', + uid: DASHBOARD_UID, queryParams: new URLSearchParams({ editPanel: '1' }), }); @@ -107,7 +137,7 @@ test.describe( // hashtag testing pyramid. test('Tests sorting by column', async ({ gotoDashboardPage, selectors, page }) => { const dashboardPage = await gotoDashboardPage({ - uid: 'dcb9f5e9-8066-4397-889e-864b99555dbb', + uid: DASHBOARD_UID, queryParams: new URLSearchParams({ editPanel: '1' }), }); @@ -132,7 +162,7 @@ test.describe( test('Tests filtering within a column', async ({ gotoDashboardPage, selectors, page }) => { const dashboardPage = await gotoDashboardPage({ - uid: 'dcb9f5e9-8066-4397-889e-864b99555dbb', + uid: DASHBOARD_UID, queryParams: new URLSearchParams({ editPanel: '1' }), }); @@ -140,10 +170,14 @@ test.describe( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Table - Kitchen Sink')) ).toBeVisible(); - const stateColumnHeader = page.getByRole('columnheader').filter({ hasText: 'Info' }); + await waitForTableLoad(page); + + const infoColumnIdx = await getColumnIdx(page, 'Info'); + + const stateColumnHeader = page.getByRole('columnheader').nth(infoColumnIdx); // get the first value in the "State" column, filter it out, then check that it went away. - const firstStateValue = (await (await getCell(page, 1, 1)).textContent())!; + const firstStateValue = (await (await getCell(page, 1, infoColumnIdx)).textContent())!; await stateColumnHeader .getByTestId(selectors.components.Panels.Visualization.TableNG.Filters.HeaderButton) .click(); @@ -162,7 +196,7 @@ test.describe( await expect(filterContainer).not.toBeVisible(); // did it actually filter out our value? - await expect(getCell(page, 1, 1)).resolves.not.toHaveText(firstStateValue); + await expect(getCell(page, 1, infoColumnIdx)).resolves.not.toHaveText(firstStateValue); }); test('Tests pagination, row height adjustment', async ({ gotoDashboardPage, selectors, page }) => { @@ -178,7 +212,7 @@ test.describe( }; const dashboardPage = await gotoDashboardPage({ - uid: 'dcb9f5e9-8066-4397-889e-864b99555dbb', + uid: DASHBOARD_UID, queryParams: new URLSearchParams({ editPanel: '1' }), }); @@ -251,12 +285,12 @@ test.describe( await page.getByRole('dialog').locator('#data-link-input [contenteditable="true"]').focus(); await page.getByRole('dialog').locator('#data-link-input [contenteditable="true"]').fill(url); await page.getByRole('dialog').locator('#data-link-input [contenteditable="true"]').blur(); - await page.getByRole('dialog').getByRole('button', { name: 'Save' }).click(); + await page.getByRole('dialog').locator('button[aria-disabled="false"]').filter({ hasText: 'Save' }).click(); await expect(page.getByRole('dialog')).not.toBeVisible(); }; const dashboardPage = await gotoDashboardPage({ - uid: 'dcb9f5e9-8066-4397-889e-864b99555dbb', + uid: DASHBOARD_UID, queryParams: new URLSearchParams({ editPanel: '1' }), }); @@ -271,8 +305,12 @@ test.describe( .last() .click(); + const infoColumnIdx = await getColumnIdx(page, 'Info'); + const pillColIdx = await getColumnIdx(page, 'Pills'); + const dataLinkColIdx = await getColumnIdx(page, 'Data Link'); + // Info column has a single DataLink by default. - const infoCell = await getCell(page, 1, 1); + const infoCell = await getCell(page, 1, infoColumnIdx); await expect(infoCell.locator('a')).toBeVisible(); expect(infoCell.locator('a')).toHaveAttribute('href'); expect(infoCell.locator('a')).not.toHaveAttribute('aria-haspopup'); @@ -283,6 +321,12 @@ test.describe( // add a DataLink to the whole table, all cells will now have a single link. const colCount = await page.getByRole('row').nth(1).getByRole('gridcell').count(); for (let colIdx = 0; colIdx < colCount; colIdx++) { + // - pills column currently does not support DataLinks. + // - we don't apply DataLinks to the DataLinks column itself, since they're rendered inside. + if (colIdx === pillColIdx || colIdx === dataLinkColIdx) { + continue; + } + const cell = await getCell(page, 1, colIdx); await expect(cell.locator('a')).toBeVisible(); expect(cell.locator('a')).toHaveAttribute('href'); @@ -297,12 +341,18 @@ test.describe( // loop thru the columns, click the links, observe that the tooltip appears, and close the tooltip. for (let colIdx = 0; colIdx < colCount; colIdx++) { const cell = await getCell(page, 1, colIdx); - if (colIdx === 1) { + if (colIdx === infoColumnIdx) { // the Info column should still have its single link. expect(cell.locator('a')).not.toHaveAttribute('aria-haspopup', 'menu'); continue; } + // - pills column currently does not support DataLinks. + // - we don't apply DataLinks to the DataLinks column itself, since they're rendered inside. + if (colIdx === pillColIdx || colIdx === dataLinkColIdx) { + continue; + } + await cell.locator('a').click({ force: true }); await expect(page.getByTestId(selectors.components.DataLinksActionsTooltip.tooltipWrapper)).toBeVisible(); @@ -316,7 +366,7 @@ test.describe( test('Empty Table panel', async ({ gotoDashboardPage, selectors }) => { const dashboardPage = await gotoDashboardPage({ - uid: 'dcb9f5e9-8066-4397-889e-864b99555dbb', + uid: DASHBOARD_UID, queryParams: new URLSearchParams({ editPanel: '2' }), }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx index a83a623ee88..5fafb04765a 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/DataLinksCell.tsx @@ -1,42 +1,16 @@ -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; - -import { useStyles2 } from '../../../../themes/ThemeContext'; import { DataLinksCellProps } from '../types'; import { getCellLinks } from '../utils'; export const DataLinksCell = ({ field, rowIdx }: DataLinksCellProps) => { - const styles = useStyles2(getStyles); + const links = getCellLinks(field, rowIdx); - const links = getCellLinks(field, rowIdx!); + if (!links?.length) { + return null; + } - return ( -
- {links && - links.map((link, idx) => { - return !link.href && link.onClick == null ? ( - - {link.title} - - ) : ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions - - - {link.title} - - - ); - })} -
- ); + return links.map((link, idx) => ( + + {link.title} + + )); }; - -const getStyles = (theme: GrafanaTheme2) => ({ - linkCell: css({ - userSelect: 'text', - whiteSpace: 'nowrap', - fontWeight: theme.typography.fontWeightMedium, - paddingRight: theme.spacing(1.5), - }), -}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx deleted file mode 100644 index ed66bdef90c..00000000000 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/JSONCell.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { css } from '@emotion/css'; -import { Property } from 'csstype'; - -import { GrafanaTheme2 } from '@grafana/data'; - -import { useStyles2 } from '../../../../themes/ThemeContext'; -import { MaybeWrapWithLink } from '../MaybeWrapWithLink'; -import { JSONCellProps } from '../types'; - -export const JSONCell = ({ value, justifyContent, field, rowIdx }: JSONCellProps) => { - const styles = useStyles2(getStyles, justifyContent); - - let displayValue = value; - - // Handle string values that might be JSON - if (typeof value === 'string') { - try { - const parsed = JSON.parse(value); - displayValue = JSON.stringify(parsed, null, ' '); - } catch { - displayValue = value; // Keep original if not valid JSON - } - } else { - // For non-string values, stringify them - try { - displayValue = JSON.stringify(value, null, ' '); - } catch (error) { - // Handle circular references or other stringify errors - displayValue = String(value); - } - } - - return ( -
- - {displayValue} - -
- ); -}; - -const getStyles = (theme: GrafanaTheme2, justifyContent: Property.JustifyContent) => ({ - jsonText: css({ - display: 'flex', - cursor: 'pointer', - fontFamily: 'monospace', - justifyContent: justifyContent, - }), -}); diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx index 225790cb0bb..4979981995b 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/SparklineCell.tsx @@ -12,6 +12,7 @@ import { isDataFrameWithValue, GrafanaTheme2, } from '@grafana/data'; +import { t } from '@grafana/i18n'; import { BarAlignment, GraphDrawStyle, @@ -46,10 +47,10 @@ export const defaultSparklineCellConfig: TableSparklineCellOptions = { export const SparklineCell = (props: SparklineCellProps) => { const { field, value, theme, timeRange, rowIdx, justifyContent, width } = props; const styles = useStyles2(getStyles, justifyContent); - const sparkline = getSparkline(value); + const sparkline = getSparkline(value, field); if (!sparkline) { - return <>{field.config.noValue || 'no data'}; + return <>{field.config.noValue || t('grafana-ui.table.sparkline.no-data', 'no data')}; } // Get the step from the first two values to null-fill the x-axis based on timerange @@ -117,11 +118,11 @@ export const SparklineCell = (props: SparklineCellProps) => { ); }; -function getSparkline(value: unknown): FieldSparkline | undefined { +function getSparkline(value: unknown, field: Field): FieldSparkline | undefined { if (Array.isArray(value)) { return { y: { - name: 'test', + name: `${field.name}-sparkline`, type: FieldType.number, values: value, config: {}, diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.test.tsx index 2f364bfa732..44221577315 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.test.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.test.tsx @@ -60,6 +60,15 @@ describe('TableNG Cells renderers', () => { config: {}, state: {}, display: jest.fn(() => ({ text: 'black', color: 'white', numeric: 0 })), + // @ts-ignore: this mock works fine for this test. + getLinks: jest.fn(() => [ + { + title: 'example', + href: 'http://example.com', + target: '_blank', + origin: {}, + }, + ]), }; } diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx index fe4bc7e3f0c..8d444eddf40 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/renderers.tsx @@ -11,7 +11,6 @@ import { BarGaugeCell } from './BarGaugeCell'; import { DataLinksCell } from './DataLinksCell'; import { GeoCell } from './GeoCell'; import { ImageCell } from './ImageCell'; -import { JSONCell } from './JSONCell'; import { PillCell } from './PillCell'; import { SparklineCell } from './SparklineCell'; @@ -44,10 +43,6 @@ const SPARKLINE_RENDERER: TableCellRenderer = (props) => ( /> ); -const JSON_RENDERER: TableCellRenderer = (props) => ( - -); - const GEO_RENDERER: TableCellRenderer = (props) => ( ); @@ -86,7 +81,7 @@ const CUSTOM_RENDERER: TableCellRenderer = (props) => { const CELL_RENDERERS: Record = { [TableCellDisplayMode.Sparkline]: SPARKLINE_RENDERER, [TableCellDisplayMode.Gauge]: GAUGE_RENDERER, - [TableCellDisplayMode.JSONView]: JSON_RENDERER, + [TableCellDisplayMode.JSONView]: AUTO_RENDERER, [TableCellDisplayMode.Image]: IMAGE_RENDERER, [TableCellDisplayMode.DataLinks]: DATA_LINKS_RENDERER, [TableCellDisplayMode.Actions]: ACTIONS_RENDERER, @@ -121,12 +116,7 @@ export function getAutoRendererResult(field: Field): TableCellRenderer { const firstValue = field.values[0]; if (isDataFrame(firstValue) && isTimeSeriesFrame(firstValue)) { return SPARKLINE_RENDERER; - } else { - return JSON_RENDERER; } } - if (field.type === FieldType.other) { - return JSON_RENDERER; - } return AUTO_RENDERER; } diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index c23f3a7384b..8c4e1e62959 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -60,7 +60,6 @@ import { getDefaultRowHeight, getDisplayName, getIsNestedTable, - getTextAlign, getVisibleFields, shouldTextOverflow, getApplyToRowBgFn, @@ -72,6 +71,10 @@ import { isCellInspectEnabled, getCellLinks, withDataLinksActionsTooltip, + displayJsonValue, + getAlignment, + getJustifyContent, + TextAlign, } from './utils'; type CellRootRenderer = (key: React.Key, props: CellRendererProps) => React.ReactNode; @@ -185,7 +188,6 @@ export function TableNG(props: TableNGProps) { fields: visibleFields, hasNestedFrames, defaultHeight: defaultRowHeight, - headerHeight, expandedRows, typographyCtx, }); @@ -307,7 +309,16 @@ export function TableNG(props: TableNGProps) { field.display = getDisplayProcessor({ field, theme }); } - const justifyContent = getTextAlign(field); + // attach JSONCell custom display function to JSONView cell type + if (cellType === TableCellDisplayMode.JSONView || field.type === FieldType.other) { + field.display = displayJsonValue; + } + + // For some cells, "aligning" the cell will mean aligning the inline contents of the cell with + // the text-align css property, and for others, we'll use justify-content to align the cell + // contents with flexbox. We always just get both and provide both when styling the cell. + const textAlign = getAlignment(field); + const justifyContent = getJustifyContent(textAlign); const footerStyles = getFooterStyles(justifyContent); const displayName = getDisplayName(field); const headerCellClass = getHeaderCellStyles(theme, justifyContent); @@ -332,6 +343,7 @@ export function TableNG(props: TableNGProps) { const withTooltip = withDataLinksActionsTooltip(field, cellType); const canBeColorized = cellType === TableCellDisplayMode.ColorBackground || cellType === TableCellDisplayMode.ColorText; + const isMonospace = cellType === TableCellDisplayMode.JSONView; result.colsWithTooltip[displayName] = withTooltip; @@ -344,7 +356,16 @@ export function TableNG(props: TableNGProps) { case TableCellDisplayMode.ColorBackground: case TableCellDisplayMode.ColorText: case TableCellDisplayMode.DataLinks: - cellClass = getCellStyles(theme, justifyContent, shouldWrap, shouldOverflow, withTooltip, canBeColorized); + case TableCellDisplayMode.JSONView: + cellClass = getCellStyles( + theme, + textAlign, + shouldWrap, + shouldOverflow, + canBeColorized, + isMonospace, + cellType === TableCellDisplayMode.DataLinks + ); break; } @@ -786,7 +807,21 @@ const getGridStyles = ( border: 'none', - '.rdg-summary-row': { + // add a box shadow on hover and selection for all body cells + '& > :not(.rdg-summary-row, .rdg-header-row) > .rdg-cell': { + '&:hover, &[aria-selected=true]': { + boxShadow: theme.shadows.z2, + }, + // selected cells should appear below hovered cells. + '&:hover': { + zIndex: theme.zIndex.tooltip - 2, + }, + '&[aria-selected=true]': { + zIndex: theme.zIndex.tooltip - 3, + }, + }, + + '.rdg-header-row, .rdg-summary-row': { '.rdg-cell': { zIndex: theme.zIndex.tooltip - 1, paddingInline: TABLE.CELL_PADDING, @@ -887,35 +922,39 @@ const getHeaderCellStyles = (theme: GrafanaTheme2, justifyContent: Property.Just const getCellStyles = ( theme: GrafanaTheme2, - justifyContent: Property.JustifyContent, + textAlign: TextAlign, shouldWrap: boolean, shouldOverflow: boolean, - hasTooltip: boolean, - isColorized: boolean + isColorized: boolean, + isMonospace: boolean, + // TODO: replace this with cellTypeStyles: TemplateStringsArray object + isLinkCell: boolean ) => css({ display: 'flex', alignItems: 'center', - justifyContent, + textAlign, + justifyContent: getJustifyContent(textAlign), paddingInline: TABLE.CELL_PADDING, minHeight: '100%', backgroundClip: 'padding-box !important', // helps when cells have a bg color - ...(shouldWrap && { whiteSpace: 'pre-line' }), + ...(shouldWrap && { whiteSpace: isMonospace ? 'pre' : 'pre-line' }), + ...(isMonospace && { fontFamily: 'monospace' }), '&:last-child': { borderInlineEnd: 'none', }, // should omit if no cell actions, and no shouldOverflow - '&:hover': { + '&:hover, &[aria-selected=true]': { '.table-cell-actions': { display: 'flex', }, ...(shouldOverflow && { - zIndex: theme.zIndex.tooltip - 2, whiteSpace: 'pre-line', height: 'fit-content', minWidth: 'fit-content', + ...(isMonospace && { whiteSpace: 'pre' }), }), }, @@ -934,4 +973,22 @@ const getCellStyles = ( }, }), }, + + ...(isLinkCell && { + '> a': { + // display: 'inline', // textWrap ? 'block' : 'inline', + whiteSpace: 'nowrap', + paddingInline: theme.spacing(1), + borderRight: `2px solid ${theme.colors.border.medium}`, + + '&:first-of-type': { + paddingInlineStart: 0, + }, + + '&:last-of-type': { + borderRight: 'none', + paddingInlineEnd: 0, + }, + }, + }), }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts index 0533c3cf3d0..a11be3d165a 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts @@ -453,7 +453,6 @@ interface UseRowHeightOptions { fields: Field[]; hasNestedFrames: boolean; defaultHeight: number; - headerHeight: number; expandedRows: Set; typographyCtx: TypographyCtx; } @@ -463,7 +462,6 @@ export function useRowHeight({ fields, hasNestedFrames, defaultHeight, - headerHeight, expandedRows, typographyCtx: { calcRowHeight, avgCharWidth }, }: UseRowHeightOptions): number | ((row: TableRow) => number) { diff --git a/packages/grafana-ui/src/components/Table/TableNG/types.ts b/packages/grafana-ui/src/components/Table/TableNG/types.ts index 742d495b806..bee921c1c71 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/types.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/types.ts @@ -219,13 +219,6 @@ export interface ImageCellProps { rowIdx: number; } -export interface JSONCellProps { - justifyContent: Property.JustifyContent; - value: TableCellValue; - field: Field; - rowIdx: number; -} - export interface DataLinksCellProps { field: Field; rowIdx: number; diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts index 59e58541744..9f48dba7b42 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts @@ -25,129 +25,85 @@ import { getComparator, getDefaultRowHeight, getIsNestedTable, - getTextAlign, + getAlignment, + getJustifyContent, migrateTableDisplayModeToCellOptions, getColumnTypes, getMaxWrapCell, } from './utils'; describe('TableNG utils', () => { - describe('text alignment', () => { - it('should map alignment options to flex values', () => { - // Test 'left' alignment - const leftField = { - name: 'Value', - type: FieldType.string, - values: [], - config: { - custom: { - align: 'left', + describe('alignment', () => { + it.each(['left', 'center', 'right'] as const)('should return "%s" when configured', (align) => { + expect( + getAlignment({ + name: 'Value', + type: FieldType.string, + values: [], + config: { + custom: { + align, + }, }, - }, - }; - expect(getTextAlign(leftField)).toBe('flex-start'); + }) + ).toBe(align); + }); - // Test 'center' alignment - const centerField = { - name: 'Value', - type: FieldType.string, - values: [], - config: { - custom: { - align: 'center', + it.each([ + { type: FieldType.string, align: 'left' }, + { type: FieldType.number, align: 'right' }, + { type: FieldType.boolean, align: 'left' }, + { type: FieldType.time, align: 'left' }, + ])('should return "$align" for field type $type by default', ({ type, align }) => { + expect( + getAlignment({ + name: 'Test', + type, + values: [], + config: { + custom: {}, }, - }, - }; - expect(getTextAlign(centerField)).toBe('center'); + }) + ).toBe(align); + }); - // Test 'right' alignment - const rightField = { - name: 'Value', - type: FieldType.string, - values: [], - config: { - custom: { - align: 'right', + it.each([ + { cellType: undefined, align: 'right' }, + { cellType: TableCellDisplayMode.Auto, align: 'right' }, + { cellType: TableCellDisplayMode.ColorText, align: 'right' }, + { cellType: TableCellDisplayMode.ColorBackground, align: 'right' }, + { cellType: TableCellDisplayMode.Gauge, align: 'left' }, + { cellType: TableCellDisplayMode.JSONView, align: 'left' }, + { cellType: TableCellDisplayMode.DataLinks, align: 'left' }, + ])('numeric field should return "$align" for cell type "$cellType"', ({ align, cellType }) => { + expect( + getAlignment({ + name: 'Test', + type: FieldType.number, + values: [], + config: { + custom: { + ...(cellType !== undefined + ? { + cellOptions: { + type: cellType, + }, + } + : {}), + }, }, - }, - }; - expect(getTextAlign(rightField)).toBe('flex-end'); + }) + ).toBe(align); }); - it('should default to flex-start when no alignment specified', () => { - const field = { - name: 'Value', - type: FieldType.string, - values: [], - config: { - custom: {}, - }, - }; - expect(getTextAlign(field)).toBe('flex-start'); - }); - - it('should default to flex-start when no field is specified', () => { - expect(getTextAlign(undefined)).toBe('flex-start'); - }); - - it('should default to flex-end for number types', () => { - const field = { - name: 'Value', - type: FieldType.number, - values: [], - config: { - custom: {}, - }, - }; - expect(getTextAlign(field)).toBe('flex-end'); - }); - - it('should default to flex-start for string types', () => { - const field = { - name: 'String', - type: FieldType.string, - values: [], - config: { - custom: {}, - }, - }; - expect(getTextAlign(field)).toBe('flex-start'); - }); - - it('should default to flex-start for enum types', () => { - const field = { - name: 'Enum', - type: FieldType.enum, - values: [], - config: { - custom: {}, - }, - }; - expect(getTextAlign(field)).toBe('flex-start'); - }); - - it('should default to flex-start for time types', () => { - const field = { - name: 'Time', - type: FieldType.time, - values: [], - config: { - custom: {}, - }, - }; - expect(getTextAlign(field)).toBe('flex-start'); - }); - - it('should default to flex-start for boolean types', () => { - const field = { - name: 'Active', - type: FieldType.boolean, - values: [], - config: { - custom: {}, - }, - }; - expect(getTextAlign(field)).toBe('flex-start'); + describe('mapping to getJustifyContent', () => { + it.each([ + { align: 'left', expected: 'flex-start' }, + { align: 'center', expected: 'center' }, + { align: 'right', expected: 'flex-end' }, + ] as const)(`should map align "$align" to justifyContent "$expected"`, ({ align, expected }) => { + expect(getJustifyContent(align)).toBe(expected); + }); }); }); @@ -919,6 +875,21 @@ describe('TableNG utils', () => { expect(onClickHandler).not.toHaveBeenCalled(); } ); + + it('should filter out links which contain neither href nor onClick', () => { + const field: Field = { + name: 'test', + type: FieldType.string, + config: {}, + values: ['value1'], + getLinks: (): LinkModel[] => [ + { title: 'Invalid Link', target: '_blank', origin: { datasourceUid: 'test' } } as LinkModel, // No href or onClick + ], + }; + + const links = getCellLinks(field, 0); + expect(links).toEqual([]); + }); }); describe('extractPixelValue', () => { diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index e0b83527470..7ba9cabf855 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -11,9 +11,11 @@ import { LinkModel, DisplayValueAlignmentFactors, DataFrame, + DisplayProcessor, } from '@grafana/data'; import { BarGaugeDisplayMode, + FieldTextAlignment, TableCellBackgroundDisplayMode, TableCellDisplayMode, TableCellHeight, @@ -23,10 +25,10 @@ import { getTextColorForAlphaBackground } from '../../../utils/colors'; import { TableCellOptions } from '../types'; import { COLUMN, TABLE } from './constants'; -import { CellColors, TableRow, TableFieldOptionsType, ColumnTypes, FrameToRowsConverter, Comparator } from './types'; +import { CellColors, TableRow, ColumnTypes, FrameToRowsConverter, Comparator } from './types'; /* ---------------------------- Cell calculations --------------------------- */ -export type CellHeightCalculator = (text: string, cellWidth: number) => number; +export type CellNumLinesCalculator = (text: string, cellWidth: number) => number; /** * @internal @@ -128,46 +130,51 @@ export function getMaxWrapCell( * Returns true if text overflow handling should be applied to the cell. */ export function shouldTextOverflow(field: Field): boolean { - let type = getCellOptions(field).type; - - return ( - field.type === FieldType.string && + const cellOptions = getCellOptions(field); + const eligibleCellType = // Tech debt: Technically image cells are of type string, which is misleading (kinda?) - // so we need to ensure we don't apply overflow hover states for type image - type !== TableCellDisplayMode.Image && - type !== TableCellDisplayMode.Pill && - !shouldTextWrap(field) && - !isCellInspectEnabled(field) - ); + // so we need to ensurefield.type === FieldType.string we don't apply overflow hover states for type image + (field.type === FieldType.string && + cellOptions.type !== TableCellDisplayMode.Image && + cellOptions.type !== TableCellDisplayMode.Pill) || + // regardless of the underlying cell type, data links cells have text overflow. + cellOptions.type === TableCellDisplayMode.DataLinks; + + return eligibleCellType && !shouldTextWrap(field) && !isCellInspectEnabled(field); +} + +// we only want to infer justifyContent and textAlign for these cellTypes +const TEXT_CELL_TYPES = new Set([ + TableCellDisplayMode.Auto, + TableCellDisplayMode.ColorText, + TableCellDisplayMode.ColorBackground, +]); + +export type TextAlign = 'left' | 'right' | 'center'; + +/** + * @internal + * Returns the text-align value for inline-displayed cells for a field based on its type and configuration. + */ +export function getAlignment(field: Field): TextAlign { + const align: FieldTextAlignment | undefined = field.config.custom?.align; + + if (!align || align === 'auto') { + if (TEXT_CELL_TYPES.has(getCellOptions(field).type) && field.type === FieldType.number) { + return 'right'; + } + return 'left'; + } + + return align; } /** * @internal - * Returns the text alignment for a field based on its type and configuration. + * Returns the justify-content value for flex-displayed cells for a field based on its type and configuration. */ -export function getTextAlign(field?: Field): Property.JustifyContent { - if (!field) { - return 'flex-start'; - } - - if (field.config.custom) { - const custom: TableFieldOptionsType = field.config.custom; - - switch (custom.align) { - case 'right': - return 'flex-end'; - case 'left': - return 'flex-start'; - case 'center': - return 'center'; - } - } - - if (field.type === FieldType.number) { - return 'flex-end'; - } - - return 'flex-start'; +export function getJustifyContent(textAlign: TextAlign): Property.JustifyContent { + return textAlign === 'center' ? 'center' : textAlign === 'right' ? 'flex-end' : 'flex-start'; } const DEFAULT_CELL_OPTIONS = { type: TableCellDisplayMode.Auto } as const; @@ -628,3 +635,27 @@ export function withDataLinksActionsTooltip(field: Field, cellType: TableCellDis (field.config.links?.length ?? 0) + (field.config.actions?.length ?? 0) > 1 ); } + +export const displayJsonValue: DisplayProcessor = (value: unknown): DisplayValue => { + let displayValue: string; + + // Handle string values that might be JSON + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + displayValue = JSON.stringify(parsed, null, ' '); + } catch { + displayValue = value; // Keep original if not valid JSON + } + } else { + // For non-string values, stringify them + try { + displayValue = JSON.stringify(value, null, ' '); + } catch (error) { + // Handle circular references or other stringify errors + displayValue = String(value); + } + } + + return { text: displayValue, numeric: Number.NaN }; +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 2d21e3f49bf..ddd03eccbf7 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -8600,7 +8600,10 @@ "no-data": "No data" }, "no-values-label": "No values", - "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} of {{numRows}} rows" + "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} of {{numRows}} rows", + "sparkline": { + "no-data": "no data" + } }, "tags": { "list-label": "Tags" From 4df4f9cc07896578e2cc9f778ce1621944846287 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:49:59 -0600 Subject: [PATCH 034/101] Release: update changelog for main (#108533) * Update changelog * Update changelog * jumpstart gha workflows * Update changelog * Update changelog * Update changelog * Update CHANGELOG.md update changelog * Update changelog * trigger gha workflows --------- Co-authored-by: github-actions[bot] Co-authored-by: jev forsberg Co-authored-by: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> --- CHANGELOG.md | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27666699c21..1a979b3b88e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,253 @@ + + +# 12.1.0 (2025-07-23) + +### Features and enhancements + +- **Access:** Disable role none option if advanced access control is not enabled [#107378](https://github.com/grafana/grafana/pull/107378), [@Jguer](https://github.com/Jguer) +- **Alerting:** Add OAuth2 Support for Webhook Receiver [#106302](https://github.com/grafana/grafana/pull/106302), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Add ability to import rules to GMA from Prometheus YAML [#105807](https://github.com/grafana/grafana/pull/105807), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Add details to the payload when tracking import to GMA [#106404](https://github.com/grafana/grafana/pull/106404), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Add export folder action to the new list view [#106256](https://github.com/grafana/grafana/pull/106256), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Add filters for health and contact point in Prometheus Rules api [#106580](https://github.com/grafana/grafana/pull/106580), [@moustafab](https://github.com/moustafab) +- **Alerting:** Add loading spinner for loading groups state [#106289](https://github.com/grafana/grafana/pull/106289), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Add need more info for import ui datasource field [#106364](https://github.com/grafana/grafana/pull/106364), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Add provenance to Prometheus API [#106596](https://github.com/grafana/grafana/pull/106596), [@moustafab](https://github.com/moustafab) +- **Alerting:** Add provenance to remote-ruler extension response (Enterprise) +- **Alerting:** Add simplified routing metadata to the details tab [#106403](https://github.com/grafana/grafana/pull/106403), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Add state history backend to write ALERTS metric [#104361](https://github.com/grafana/grafana/pull/104361), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Add support for Redis Sentinel for Alerting HA [#106322](https://github.com/grafana/grafana/pull/106322), [@vstpme](https://github.com/vstpme) +- **Alerting:** Allow disabling recording rules write for a data source in the UI [#106664](https://github.com/grafana/grafana/pull/106664), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Correctly persist FiredAt in SyncRuleStatePersister [#106658](https://github.com/grafana/grafana/pull/106658), [@fayzal-g](https://github.com/fayzal-g) +- **Alerting:** Ensure errors cleared when Alerting after error [#105246](https://github.com/grafana/grafana/pull/105246), [@moustafab](https://github.com/moustafab) +- **Alerting:** Evaluate all imported from Prometheus rules sequentially [#106295](https://github.com/grafana/grafana/pull/106295), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Extensible Settings module [#107831](https://github.com/grafana/grafana/pull/107831), [@konrad147](https://github.com/konrad147) +- **Alerting:** Filter out rules managed by integrations and add an info alert [#106602](https://github.com/grafana/grafana/pull/106602), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Filter out synthetic datasource-managed rules when importing to GMA [#106358](https://github.com/grafana/grafana/pull/106358), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** List V2 - Add labels popup [#107193](https://github.com/grafana/grafana/pull/107193), [@konrad147](https://github.com/konrad147) +- **Alerting:** List V2 - Grouped view filters [#106400](https://github.com/grafana/grafana/pull/106400), [@konrad147](https://github.com/konrad147) +- **Alerting:** List V2 - Use backend filters for GMA rules [#106897](https://github.com/grafana/grafana/pull/106897), [@konrad147](https://github.com/konrad147) +- **Alerting:** Make paginated rules endpoint strongly consistent (Enterprise) +- **Alerting:** Optimize out unnecessary permission check for rule groups (Enterprise) +- **Alerting:** Optimize prometheus api permission checks [#106299](https://github.com/grafana/grafana/pull/106299), [@moustafab](https://github.com/moustafab) +- **Alerting:** Optimize prometheus api permission checks (Enterprise) +- **Alerting:** Persist alert instance FiredAt field [#105927](https://github.com/grafana/grafana/pull/105927), [@fayzal-g](https://github.com/fayzal-g) +- **Alerting:** Remove ruler from alert list view2 [#106778](https://github.com/grafana/grafana/pull/106778), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Resend alerts for states that are missing in the eval results [#105965](https://github.com/grafana/grafana/pull/105965), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Send notifications immediately on Error|NoData -> Normal transitions [#106421](https://github.com/grafana/grafana/pull/106421), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Support PDC in Grafana-managed recording rules [#106677](https://github.com/grafana/grafana/pull/106677), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Use default_datasource_uid as the default target for recording rules in UI [#106415](https://github.com/grafana/grafana/pull/106415), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Annotations:** Use dashboard uids instead of dashboard ids [#106676](https://github.com/grafana/grafana/pull/106676), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **App Platform Provisioning:** Add experimental nanogit mode for Git Sync [#106763](https://github.com/grafana/grafana/pull/106763), [@MissingRoberto](https://github.com/MissingRoberto) +- **Auth:** Add Azure/Entra workload identity support [#104807](https://github.com/grafana/grafana/pull/104807), [@mehighlow](https://github.com/mehighlow) +- **Auth:** Enable improved session handling by default for OAuth and SAML [#107442](https://github.com/grafana/grafana/pull/107442), [@mgyongyosi](https://github.com/mgyongyosi) +- **Auth:** Enable ssoSettingsLDAP by default [#106310](https://github.com/grafana/grafana/pull/106310), [@mgyongyosi](https://github.com/mgyongyosi) +- **Auth:** Remove api key endpoints [#106019](https://github.com/grafana/grafana/pull/106019), [@dmihai](https://github.com/dmihai) +- **Auth:** Remove code for authenticating API keys [#105998](https://github.com/grafana/grafana/pull/105998), [@dmihai](https://github.com/dmihai) +- **Azure:** Support scope selection in Resource Graph queries [#105835](https://github.com/grafana/grafana/pull/105835), [@aangelisc](https://github.com/aangelisc) +- **Betterer:** Only allow singleton Storage use [#105310](https://github.com/grafana/grafana/pull/105310), [@tskarhed](https://github.com/tskarhed) +- **Caching:** Remove memcached reconnect_interval setting (Enterprise) +- **Chore:** Update k8s.io to v0.33.1 [#105307](https://github.com/grafana/grafana/pull/105307), [@ryantxu](https://github.com/ryantxu) +- **Cloud Monitoring:** Add support for service account impersonation [#107022](https://github.com/grafana/grafana/pull/107022), [@zoltanbedi](https://github.com/zoltanbedi) +- **CloudMigrations:** Add Mute Timings as dependency for Notification Policies [#106751](https://github.com/grafana/grafana/pull/106751), [@macabu](https://github.com/macabu) +- **CloudWatch:** Backport aws-sdk-go-v2 update from external plugin [#107136](https://github.com/grafana/grafana/pull/107136), [@njvrzm](https://github.com/njvrzm) +- **CloudWatch:** Improve instance attribute variable query editor [#105206](https://github.com/grafana/grafana/pull/105206), [@iwysiu](https://github.com/iwysiu) +- **Cloudwatch:** Add missing AWS regions [#106304](https://github.com/grafana/grafana/pull/106304), [@chriscerie](https://github.com/chriscerie) +- **Dashboard Provisioning:** Reduce db load [#106114](https://github.com/grafana/grafana/pull/106114), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Dashboard:** Add Alert icon in library panels [#107723](https://github.com/grafana/grafana/pull/107723), [@axelavargas](https://github.com/axelavargas) +- **Dashboard:** Add server-configurable quick ranges for the time picker [#102254](https://github.com/grafana/grafana/pull/102254), [@chodges15](https://github.com/chodges15) +- **Dashboard:** Formatting Currency - add new custom 'financial' currency format without abbreviations [#106604](https://github.com/grafana/grafana/pull/106604), [@axelavargas](https://github.com/axelavargas) +- **Dashboard:** Library Panels - Add ability to search by folder name [#106997](https://github.com/grafana/grafana/pull/106997), [@axelavargas](https://github.com/axelavargas) +- **Dashboard:** Schema V2 - Auto-transform V2 dashboards in V1Resource export mode [#105997](https://github.com/grafana/grafana/pull/105997), [@axelavargas](https://github.com/axelavargas) +- **Datasources:** Migrate to new sigv4 middleware (Enterprise) +- **Datasources:** Update grafana-aws-sdk for new sigv4 middleware and aws-sdk-go v1 removal [#107522](https://github.com/grafana/grafana/pull/107522), [@njvrzm](https://github.com/njvrzm) +- **DatePicker:** Add cursor not-allowed style and hover background color [#106451](https://github.com/grafana/grafana/pull/106451), [@ywzheng1](https://github.com/ywzheng1) +- **Dependencies:** Bump Go to v1.24.4 [#106533](https://github.com/grafana/grafana/pull/106533), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.3.0 [#107379](https://github.com/grafana/grafana/pull/107379), [@macabu](https://github.com/macabu) +- **Dependencies:** Bump github.com/openfga/openfga to v1.8.13 to address CVE-2025-48371 [#106064](https://github.com/grafana/grafana/pull/106064), [@macabu](https://github.com/macabu) +- **ElasticSearch:** Remove frontend response parsing [#104148](https://github.com/grafana/grafana/pull/104148), [@nojaf](https://github.com/nojaf) +- **Geomap:** Add HiDPI support to CARTO basemap (#81195) [#106211](https://github.com/grafana/grafana/pull/106211), [@cledwynl](https://github.com/cledwynl) +- **Git Sync UI:** Delete Provisioned Dashboard Flow [#106593](https://github.com/grafana/grafana/pull/106593), [@ywzheng1](https://github.com/ywzheng1) +- **Grafana/data:** Extract fuzzy search core [#107110](https://github.com/grafana/grafana/pull/107110), [@Clarity-89](https://github.com/Clarity-89) +- **I18n:** Update eslint rule to catch some untranslated object properties [#105072](https://github.com/grafana/grafana/pull/105072), [@tomratcliffe](https://github.com/tomratcliffe) +- **InfluxDB:** Add an optional time range filter for tag queries in the query panel autocompleteInflux tag filter [#107195](https://github.com/grafana/grafana/pull/107195), [@NikolayTsvetkov](https://github.com/NikolayTsvetkov) +- **LBAC for data sources:** Adds team filtering for lbac rules (Enterprise) +- **Library Panels:** Mark library panel RBAC as GA & enable by default [#106833](https://github.com/grafana/grafana/pull/106833), [@kaydelaney](https://github.com/kaydelaney) +- **Library Panels:** Modify connection api endpoint to be compatible with unified storage [#107088](https://github.com/grafana/grafana/pull/107088), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Library elements:** Remove ability to set as "library variable" [#106594](https://github.com/grafana/grafana/pull/106594), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Library panels:** Remove `libraryPanelRBAC` feature flag, and enable rbac by default [#107222](https://github.com/grafana/grafana/pull/107222), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Loki:** Remove experimental lokiQuerySplittingConfig [#107298](https://github.com/grafana/grafana/pull/107298), [@ivanahuckova](https://github.com/ivanahuckova) +- **Loki:** Remove experimental predefined operations [#107289](https://github.com/grafana/grafana/pull/107289), [@ivanahuckova](https://github.com/ivanahuckova) +- **OAuth:** Add access token as third source for user info extraction [#107636](https://github.com/grafana/grafana/pull/107636), [@Jguer](https://github.com/Jguer) +- **Plugin Extensions:** Expose PluginMeta generic in usePluginContext [#107577](https://github.com/grafana/grafana/pull/107577), [@MattIPv4](https://github.com/MattIPv4) +- **Postgres:** Switch the datasource plugin from lib/pq to pgx [#103961](https://github.com/grafana/grafana/pull/103961), [@zoltanbedi](https://github.com/zoltanbedi) +- **Preferences:** Use dashboard uid for the home dashboard [#106666](https://github.com/grafana/grafana/pull/106666), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Profiles:** Stop passing response headers for Grafana-Pyroscope and parca datasources [#106577](https://github.com/grafana/grafana/pull/106577), [@simonswine](https://github.com/simonswine) +- **Prometheus:** Deprecation message for Azure auth [#106490](https://github.com/grafana/grafana/pull/106490), [@bossinc](https://github.com/bossinc) +- **Prometheus:** Facilitate tree shaking with exports and bundler mode [#105575](https://github.com/grafana/grafana/pull/105575), [@NWRichmond](https://github.com/NWRichmond) +- **Prometheus:** Migrate remaining selectors to data-testid [#106564](https://github.com/grafana/grafana/pull/106564), [@idastambuk](https://github.com/idastambuk) +- **ProvisionedFolder:** Delete folder drawer [#107089](https://github.com/grafana/grafana/pull/107089), [@ywzheng1](https://github.com/ywzheng1) +- **Provisioning:** Add pure git repository type [#106815](https://github.com/grafana/grafana/pull/106815), [@MissingRoberto](https://github.com/MissingRoberto) +- **Querying:** Pass dashboard and panel title as headers [#107032](https://github.com/grafana/grafana/pull/107032), [@ivanahuckova](https://github.com/ivanahuckova) +- **Remote Alertmanager:** Send SMTP config [#106337](https://github.com/grafana/grafana/pull/106337), [@santihernandezc](https://github.com/santihernandezc) +- **Restore dashboards:** Add filters and search [#106994](https://github.com/grafana/grafana/pull/106994), [@Clarity-89](https://github.com/Clarity-89) +- **SCIM:** Ignore unsupported fields in user PATCH requests (Enterprise) +- **SCIM:** Implement operation for adding an externalId value to a team (Enterprise) +- **SCIM:** Implement the add members operation in group PATCH requests (Enterprise) +- **SCIM:** Implement the remove members operation in group PATCH requests (Enterprise) +- **SCIM:** Update externalId field in group PATCH request (Enterprise) +- **SQL Expressions:** Always convert on type first [#106083](https://github.com/grafana/grafana/pull/106083), [@kylebrandt](https://github.com/kylebrandt) +- **Select:** Set min width for the current selected item when width=auto [#106131](https://github.com/grafana/grafana/pull/106131), [@tskarhed](https://github.com/tskarhed) +- **StateTimeline:** Display false and empty string values [#107059](https://github.com/grafana/grafana/pull/107059), [@jesdavpet](https://github.com/jesdavpet) +- **StateTimeline:** Support `NaN` and `null` value mappings [#105638](https://github.com/grafana/grafana/pull/105638), [@fastfrwrd](https://github.com/fastfrwrd) +- **Storage:** Take `migration_locking` setting into account [#105938](https://github.com/grafana/grafana/pull/105938), [@JohnnyQQQQ](https://github.com/JohnnyQQQQ) +- **TableNG:** Refactor to better take advantage of react-data-grid [#103755](https://github.com/grafana/grafana/pull/103755), [@leeoniya](https://github.com/leeoniya) +- **Tables:** Pills for Table Cells [#107485](https://github.com/grafana/grafana/pull/107485), [@timlevett](https://github.com/timlevett) +- **Teams:** Add support for updating externalId field [#106406](https://github.com/grafana/grafana/pull/106406), [@dmihai](https://github.com/dmihai) +- **Tempo:** Enable native histograms for Tempo service graph [#105989](https://github.com/grafana/grafana/pull/105989), [@bohandley](https://github.com/bohandley) +- **TimeRangePicker:** Highlight range on hover [#106616](https://github.com/grafana/grafana/pull/106616), [@joshhunt](https://github.com/joshhunt) +- **TraceView:** Resource attributes links extension point [#104680](https://github.com/grafana/grafana/pull/104680), [@edvard-falkskar](https://github.com/edvard-falkskar) +- **Transformations:** Add "Auto" mode to Organize Fields [#103055](https://github.com/grafana/grafana/pull/103055), [@gelicia](https://github.com/gelicia) +- **Transformations:** GA the Regression transformation [#106074](https://github.com/grafana/grafana/pull/106074), [@gelicia](https://github.com/gelicia) +- **Unified storage:** Respect GF_DATABASE_URL override [#105331](https://github.com/grafana/grafana/pull/105331), [@pstibrany](https://github.com/pstibrany) +- **VQB:** Add selected columns to GROUP BY dropdown (#106349) [#106391](https://github.com/grafana/grafana/pull/106391), [@Shubham19032004](https://github.com/Shubham19032004) +- **VQB:** Allow custom table names in TableSelector [#106420](https://github.com/grafana/grafana/pull/106420), [@Victorthedev](https://github.com/Victorthedev) +- **XYChart:** Add support for x=time [#106459](https://github.com/grafana/grafana/pull/106459), [@leeoniya](https://github.com/leeoniya) + +### Bug fixes + +- **Alerting:** Fix $value type when single data source is queried [#106080](https://github.com/grafana/grafana/pull/106080), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Fix ImportToGMARules flaky test [#106495](https://github.com/grafana/grafana/pull/106495), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix RefIds not being shown when creating or editing Grafana-managed recording rule [#106840](https://github.com/grafana/grafana/pull/106840), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **Alerting:** Fix contact points tab visibility when user can only create [#106735](https://github.com/grafana/grafana/pull/106735), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Fix eval time unit in list view [#106488](https://github.com/grafana/grafana/pull/106488), [@ebuildy](https://github.com/ebuildy) +- **Alerting:** Fix group interval override when adding new rules [#107324](https://github.com/grafana/grafana/pull/107324), [@konrad147](https://github.com/konrad147) +- **Alerting:** Fix group-level labels and query_offset in the import API [#106379](https://github.com/grafana/grafana/pull/106379), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Fix notification policy conflicts originating from provenance mismatch [#107343](https://github.com/grafana/grafana/pull/107343), [@moustafab](https://github.com/moustafab) +- **Alerting:** Fix resolved notifications for same-label Error to Normal transitions [#106210](https://github.com/grafana/grafana/pull/106210), [@alexander-akhmetov](https://github.com/alexander-akhmetov) +- **Alerting:** Hide labels section if we only have private labels [#105996](https://github.com/grafana/grafana/pull/105996), [@gillesdemey](https://github.com/gillesdemey) +- **Annotations:** Remove prometheus from legacy runner [#106737](https://github.com/grafana/grafana/pull/106737), [@scottlepp](https://github.com/scottlepp) +- **Azure:** Fix Application Insights metadata requests [#105614](https://github.com/grafana/grafana/pull/105614), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix duplicated trace links [#105698](https://github.com/grafana/grafana/pull/105698), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix legend formatting [#106504](https://github.com/grafana/grafana/pull/106504), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix resource name determination in template variable queries [#105705](https://github.com/grafana/grafana/pull/105705), [@aangelisc](https://github.com/aangelisc) +- **BarChart/StateTimeline:** Use noValue setting for error message when data is empty [#107147](https://github.com/grafana/grafana/pull/107147), [@fastfrwrd](https://github.com/fastfrwrd) +- **CloudWatch:** Fix http client handling + assume role bug [#107893](https://github.com/grafana/grafana/pull/107893), [@njvrzm](https://github.com/njvrzm) +- **CloudWatch:** Fix proxy transport issue [#107807](https://github.com/grafana/grafana/pull/107807), [@njvrzm](https://github.com/njvrzm) +- **Dashboard:** FF `dashboardNewLayouts` Fix library panels non-editable when multiple added [#107052](https://github.com/grafana/grafana/pull/107052), [@axelavargas](https://github.com/axelavargas) +- **Dashboard:** Fix cache validation to prevent stale cache [#105918](https://github.com/grafana/grafana/pull/105918), [@yashschandra](https://github.com/yashschandra) +- **Dashboard:** Fixes issue with dashboard links that include all variables [#106356](https://github.com/grafana/grafana/pull/106356), [@torkelo](https://github.com/torkelo) +- **Dashboards:** Fix history list for dashboard uids that end in `-` [#107073](https://github.com/grafana/grafana/pull/107073), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Drilldown:** Fix js crash when using http [#105646](https://github.com/grafana/grafana/pull/105646), [@chu121su12](https://github.com/chu121su12) +- **Fix:** Increase login_attempt.ip_address column length for IPv6 support [#107035](https://github.com/grafana/grafana/pull/107035), [@Jguer](https://github.com/Jguer) +- **FlameGraph:** Fix bug for function names that conflict with JavaScript object prototype properties [#106338](https://github.com/grafana/grafana/pull/106338), [@simonswine](https://github.com/simonswine) +- **Folders:** Correctly resolve nested folder breadcrumbs [#106344](https://github.com/grafana/grafana/pull/106344), [@IevaVasiljeva](https://github.com/IevaVasiljeva) +- **GrafanaUI:** Fix Combobox ignoring loading prop [#105584](https://github.com/grafana/grafana/pull/105584), [@ValeraS](https://github.com/ValeraS) +- **Graphite:** Fix annotation queries [#106553](https://github.com/grafana/grafana/pull/106553), [@aangelisc](https://github.com/aangelisc) +- **Graphite:** Fix date mutation [#107414](https://github.com/grafana/grafana/pull/107414), [@aangelisc](https://github.com/aangelisc) +- **Graphite:** Fix nested variable interpolation for repeated rows [#106976](https://github.com/grafana/grafana/pull/106976), [@aangelisc](https://github.com/aangelisc) +- **K8s:** Dashboards /apis: Fix library element connections [#106734](https://github.com/grafana/grafana/pull/106734), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Loki:** Fix health check message [#107170](https://github.com/grafana/grafana/pull/107170), [@wooffie](https://github.com/wooffie) +- **Loki:** Fix issue where step parameter using a template variable was marked as invalid [#106541](https://github.com/grafana/grafana/pull/106541), [@ivanahuckova](https://github.com/ivanahuckova) +- **Loki:** Fix label browser not sorted after selection of a label [#107394](https://github.com/grafana/grafana/pull/107394), [@paulojmdias](https://github.com/paulojmdias) +- **Org:** Fix org deletion [#106193](https://github.com/grafana/grafana/pull/106193), [@stephaniehingtgen](https://github.com/stephaniehingtgen) +- **Plugins:** Fix and encode invalid gRPC header values [#107339](https://github.com/grafana/grafana/pull/107339), [@ivanahuckova](https://github.com/ivanahuckova) +- **PostgreSQL:** Fix error on panel when toggling sqlDatasourceDatabaseSelection feature [#106965](https://github.com/grafana/grafana/pull/106965), [@HasithDeAlwis](https://github.com/HasithDeAlwis) +- **Profiles:** Fix for passing the response headers [#106293](https://github.com/grafana/grafana/pull/106293), [@simonswine](https://github.com/simonswine) +- **Reporting:** Stop sending reports with Never schedule on creation (Enterprise) +- **SCIM:** Fix PUT request for deactivating a user (Enterprise) +- **SCIM:** Fix the removal of all members in group PUT requests (Enterprise) +- **SCIM:** Fix user patch operation (Enterprise) +- **Security:** Add fix for CVE-2025-3580 [#105976](https://github.com/grafana/grafana/pull/105976), [@baldm0mma](https://github.com/baldm0mma) +- **Security:** Fixes for CVE-2025-6197 and CVE-2025-6023 [#108333](https://github.com/grafana/grafana/pull/108333), [@mgyongyosi](https://github.com/mgyongyosi) +- **Settings:** Fix reencryption and rollback of encrypted values in setting table (Enterprise) +- **Tempo:** Fix showing dangling edges in NodeGraph [#107245](https://github.com/grafana/grafana/pull/107245), [@ifrost](https://github.com/ifrost) +- **ToolTip:** Fix flexbox bug with tooltip when `maxWidth` is set manually [#107145](https://github.com/grafana/grafana/pull/107145), [@jdmarshall](https://github.com/jdmarshall) +- **URLParams:** Stringify true values as key=true always (fixes issues with variables with true value) [#106440](https://github.com/grafana/grafana/pull/106440), [@torkelo](https://github.com/torkelo) + +### Breaking changes + +- **Alerting:** Enable recording rules by default [#105603](https://github.com/grafana/grafana/pull/105603), [@alexander-akhmetov](https://github.com/alexander-akhmetov) + +### Plugin development fixes & changes + +- **Carousel:** Always center image [#106468](https://github.com/grafana/grafana/pull/106468), [@ashharrison90](https://github.com/ashharrison90) +- **Drawer:** Include divider and close button when passing a custom title element [#106896](https://github.com/grafana/grafana/pull/106896), [@ashharrison90](https://github.com/ashharrison90) + + + + +# 12.0.3 (2025-07-23) + +### Bug fixes + +- **Security:** Fixes for CVE-2025-6197 and CVE-2025-6023 [#108280](https://github.com/grafana/grafana/pull/108280), [@volcanonoodle](https://github.com/volcanonoodle) + + + + +# 11.6.4 (2025-07-23) + +### Features and enhancements + +- **Dependencies:** Bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.3.0 [#107555](https://github.com/grafana/grafana/pull/107555), [@macabu](https://github.com/macabu) +- **StateTimeline:** Add endTime to tooltip [#107605](https://github.com/grafana/grafana/pull/107605), [@adela-almasan](https://github.com/adela-almasan) +- **Unified storage:** Respect GF_DATABASE_URL override [#107573](https://github.com/grafana/grafana/pull/107573), [@pstibrany](https://github.com/pstibrany) + +### Bug fixes + +- **Alerting:** Fix group interval override when adding new rules [#107496](https://github.com/grafana/grafana/pull/107496), [@konrad147](https://github.com/konrad147) +- **Azure:** Fix legend formatting [#106934](https://github.com/grafana/grafana/pull/106934), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix resource name determination in template variable queries [#106939](https://github.com/grafana/grafana/pull/106939), [@aangelisc](https://github.com/aangelisc) +- **Graphite:** Fix annotation queries [#106940](https://github.com/grafana/grafana/pull/106940), [@aangelisc](https://github.com/aangelisc) +- **Graphite:** Fix date mutation [#107523](https://github.com/grafana/grafana/pull/107523), [@aangelisc](https://github.com/aangelisc) +- **Graphite:** Fix nested variable interpolation for repeated rows [#107564](https://github.com/grafana/grafana/pull/107564), [@aangelisc](https://github.com/aangelisc) +- **Security:** Fixes for CVE-2025-6197 and CVE-2025-6023 [#108281](https://github.com/grafana/grafana/pull/108281), [@volcanonoodle](https://github.com/volcanonoodle) + + + + +# 11.5.7 (2025-07-23) + +### Features and enhancements + +- **Dependencies:** Bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.3.0 [#107556](https://github.com/grafana/grafana/pull/107556), [@macabu](https://github.com/macabu) + +### Bug fixes + +- **Azure:** Fix legend formatting [#106933](https://github.com/grafana/grafana/pull/106933), [@aangelisc](https://github.com/aangelisc) +- **Azure:** Fix resource name determination in template variable queries [#106938](https://github.com/grafana/grafana/pull/106938), [@aangelisc](https://github.com/aangelisc) +- **Security:** Fixes for CVE-2025-6197 and CVE-2025-6023 [#108280](https://github.com/grafana/grafana/pull/108280), [@volcanonoodle](https://github.com/volcanonoodle) + + + + +# 11.4.7 (2025-07-23) + +### Features and enhancements + +- **Dependencies:** Bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.3.0 [#107557](https://github.com/grafana/grafana/pull/107557), [@macabu](https://github.com/macabu) + +### Bug fixes + +- **Azure:** Fix legend formatting [#106932](https://github.com/grafana/grafana/pull/106932), [@aangelisc](https://github.com/aangelisc) +- **Security:** Fixes for CVE-2025-6197 and CVE-2025-6023 [#108279](https://github.com/grafana/grafana/pull/108279), [@volcanonoodle](https://github.com/volcanonoodle) + + + + +# 11.3.9 (2025-07-23) + +### Features and enhancements + +- **Dependencies:** Bump github.com/go-viper/mapstructure/v2 from 2.2.1 to 2.3.0 [#107558](https://github.com/grafana/grafana/pull/107558), [@macabu](https://github.com/macabu) + +### Bug fixes + +- **Azure:** Fix legend formatting [#106931](https://github.com/grafana/grafana/pull/106931), [@aangelisc](https://github.com/aangelisc) +- **Security:** Fixes for CVE-2025-6197 and CVE-2025-6023 [#108278](https://github.com/grafana/grafana/pull/108278), [@volcanonoodle](https://github.com/volcanonoodle) + + # 12.0.2+security-01 (2025-07-17) From 981fdb29d42b466cb480331372ddabb8b5fefcc2 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:59:24 -0400 Subject: [PATCH 035/101] update storage-api to only build index if it owns the namespace (#108418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update storage-api to only build index if it owns the namespace --------- Co-authored-by: Mustafa Sencer Özcan --- pkg/storage/unified/resource/search.go | 41 ++++++++++++++++++- .../resource/search_server_distributor.go | 13 ++++-- pkg/storage/unified/resource/search_test.go | 4 +- pkg/storage/unified/resource/server.go | 8 +++- pkg/storage/unified/sql/server.go | 5 +++ pkg/storage/unified/sql/service.go | 18 ++++---- 6 files changed, 73 insertions(+), 16 deletions(-) diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 0a88d9a035a..96a44f6d3c3 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -4,6 +4,7 @@ import ( "cmp" "context" "fmt" + "hash/fnv" "log/slog" "slices" "strings" @@ -19,6 +20,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/grafana/authlib/types" + "github.com/grafana/dskit/ring" dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" @@ -109,6 +111,9 @@ type searchSupport struct { initMinSize int initMaxSize int + ring *ring.Ring + ringLifecycler *ring.BasicLifecycler + buildIndex singleflight.Group // Index queue processors @@ -128,7 +133,7 @@ var ( _ resourcepb.ManagedObjectIndexServer = (*searchSupport)(nil) ) -func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.AccessClient, blob BlobSupport, tracer trace.Tracer, indexMetrics *BleveIndexMetrics) (support *searchSupport, err error) { +func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.AccessClient, blob BlobSupport, tracer trace.Tracer, indexMetrics *BleveIndexMetrics, ring *ring.Ring, ringLifecycler *ring.BasicLifecycler) (support *searchSupport, err error) { // No backend search support if opts.Backend == nil { return nil, nil @@ -155,6 +160,8 @@ func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.A indexEventsChan: make(chan *IndexEvent), indexQueueProcessors: make(map[string]*indexQueueProcessor), rebuildInterval: opts.RebuildInterval, + ring: ring, + ringLifecycler: ringLifecycler, } info, err := opts.Resources.GetDocumentBuilders() @@ -379,6 +386,33 @@ func (s *searchSupport) GetStats(ctx context.Context, req *resourcepb.ResourceSt return rsp, nil } +func (s *searchSupport) shouldBuildIndex(info ResourceStats) bool { + if s.ring == nil { + s.log.Debug("ring is not setup. Will proceed to build index") + return true + } + + if s.ringLifecycler == nil { + s.log.Error("missing ring lifecycler") + return true + } + + ringHasher := fnv.New32a() + _, err := ringHasher.Write([]byte(info.Namespace)) + if err != nil { + s.log.Error("error hashing namespace", "namespace", info.Namespace, "err", err) + return true + } + + rs, err := s.ring.GetWithOptions(ringHasher.Sum32(), searchOwnerRead, ring.WithReplicationFactor(s.ring.ReplicationFactor())) + if err != nil { + s.log.Error("error getting replicaset from ring", "namespace", info.Namespace, "err", err) + return true + } + + return rs.Includes(s.ringLifecycler.GetInstanceAddr()) +} + func (s *searchSupport) buildIndexes(ctx context.Context, rebuild bool) (int, error) { totalBatchesIndexed := 0 group := errgroup.Group{} @@ -395,6 +429,11 @@ func (s *searchSupport) buildIndexes(ctx context.Context, rebuild bool) (int, er continue } + if !s.shouldBuildIndex(info) { + s.log.Debug("skip building index", "namespace", info.Namespace, "group", info.Group, "resource", info.Resource) + continue + } + group.Go(func() error { if rebuild { // we need to clear the cache to make sure we get the latest usage insights data diff --git a/pkg/storage/unified/resource/search_server_distributor.go b/pkg/storage/unified/resource/search_server_distributor.go index c148448ca7b..f410207b59e 100644 --- a/pkg/storage/unified/resource/search_server_distributor.go +++ b/pkg/storage/unified/resource/search_server_distributor.go @@ -81,9 +81,14 @@ type distributorServer struct { log log.Logger } -var activeRingOp = ring.NewOp([]ring.InstanceState{ring.ACTIVE}, func(s ring.InstanceState) bool { - return s != ring.ACTIVE -}) +var ( + // operation used by the distributor to select only ACTIVE instances to handle search-related requests + searchRingRead = ring.NewOp([]ring.InstanceState{ring.ACTIVE}, func(s ring.InstanceState) bool { + return s != ring.ACTIVE + }) + // operation used by the search-servers to check if they own the namespace + searchOwnerRead = ring.NewOp([]ring.InstanceState{ring.JOINING, ring.ACTIVE, ring.LEAVING}, nil) +) func (ds *distributorServer) Search(ctx context.Context, r *resourcepb.ResourceSearchRequest) (*resourcepb.ResourceSearchResponse, error) { ctx, client, err := ds.getClientToDistributeRequest(ctx, r.Options.Key.Namespace, "Search") @@ -128,7 +133,7 @@ func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, n return ctx, nil, err } - rs, err := ds.ring.GetWithOptions(ringHasher.Sum32(), activeRingOp, ring.WithReplicationFactor(ds.ring.ReplicationFactor())) + rs, err := ds.ring.GetWithOptions(ringHasher.Sum32(), searchRingRead, ring.WithReplicationFactor(ds.ring.ReplicationFactor())) if err != nil { return ctx, nil, err } diff --git a/pkg/storage/unified/resource/search_test.go b/pkg/storage/unified/resource/search_test.go index 25f7f3280c0..ab8cee98432 100644 --- a/pkg/storage/unified/resource/search_test.go +++ b/pkg/storage/unified/resource/search_test.go @@ -246,7 +246,7 @@ func TestBuildIndexes_MaxCountThreshold(t *testing.T) { InitMaxCount: tt.initMaxSize, } - support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil) + support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil, nil, nil) require.NoError(t, err) require.NotNil(t, support) @@ -304,7 +304,7 @@ func TestSearchGetOrCreateIndex(t *testing.T) { InitMaxCount: 0, } - support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil) + support, err := newSearchSupport(opts, storage, nil, nil, noop.NewTracerProvider().Tracer("test"), nil, nil, nil) require.NoError(t, err) require.NotNil(t, support) diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index b06ee3e7240..78bf79627da 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -20,6 +20,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/dskit/backoff" + "github.com/grafana/dskit/ring" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resourcepb" @@ -180,6 +181,8 @@ type SearchOptions struct { // Interval for periodic index rebuilds (0 disables periodic rebuilds) RebuildInterval time.Duration + + Ring *ring.Ring } type ResourceServerOptions struct { @@ -223,6 +226,9 @@ type ResourceServerOptions struct { // QOSQueue is the quality of service queue used to enqueue QOSQueue QOSEnqueuer + + Ring *ring.Ring + RingLifecycler *ring.BasicLifecycler } func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { @@ -305,7 +311,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { if opts.Search.Resources != nil { var err error - s.search, err = newSearchSupport(opts.Search, s.backend, s.access, s.blob, opts.Tracer, opts.IndexMetrics) + s.search, err = newSearchSupport(opts.Search, s.backend, s.access, s.blob, opts.Tracer, opts.IndexMetrics, opts.Ring, opts.RingLifecycler) if err != nil { return nil, err } diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index 97163958972..e89b20a0780 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -9,6 +9,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/authlib/types" + "github.com/grafana/dskit/ring" "github.com/grafana/dskit/services" infraDB "github.com/grafana/grafana/pkg/infra/db" @@ -37,6 +38,8 @@ type ServerOptions struct { IndexMetrics *resource.BleveIndexMetrics Features featuremgmt.FeatureToggles QOSQueue QOSEnqueueDequeuer + Ring *ring.Ring + RingLifecycler *ring.BasicLifecycler } // Creates a new ResourceServer @@ -96,6 +99,8 @@ func NewResourceServer( serverOptions.Search = opts.SearchOptions serverOptions.IndexMetrics = opts.IndexMetrics serverOptions.QOSQueue = opts.QOSQueue + serverOptions.Ring = opts.Ring + serverOptions.RingLifecycler = opts.RingLifecycler return resource.NewResourceServer(serverOptions) } diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index c8a2ef8260d..171895cb67b 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -75,8 +75,8 @@ type service struct { docBuilders resource.DocumentBuilderSupplier - storageRing *ring.Ring - lifecycler *ring.BasicLifecycler + searchRing *ring.Ring + ringLifecycler *ring.BasicLifecycler queue QOSEnqueueDequeuer scheduler *scheduler.Scheduler @@ -91,7 +91,7 @@ func ProvideUnifiedStorageGrpcService( docBuilders resource.DocumentBuilderSupplier, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, - storageRing *ring.Ring, + searchRing *ring.Ring, memberlistKVConfig kv.Config, ) (UnifiedStorageGrpcService, error) { var err error @@ -116,7 +116,7 @@ func ProvideUnifiedStorageGrpcService( docBuilders: docBuilders, storageMetrics: storageMetrics, indexMetrics: indexMetrics, - storageRing: storageRing, + searchRing: searchRing, subservicesWatcher: services.NewFailureWatcher(), } @@ -143,7 +143,7 @@ func ProvideUnifiedStorageGrpcService( delegate = ring.NewLeaveOnStoppingDelegate(delegate, log) delegate = ring.NewAutoForgetDelegate(resource.RingHeartbeatTimeout*2, delegate, log) - s.lifecycler, err = ring.NewBasicLifecycler( + s.ringLifecycler, err = ring.NewBasicLifecycler( lifecyclerCfg, resource.RingName, resource.RingKey, @@ -155,7 +155,7 @@ func ProvideUnifiedStorageGrpcService( if err != nil { return nil, fmt.Errorf("failed to initialize storage-ring lifecycler: %s", err) } - subservices = append(subservices, s.lifecycler) + subservices = append(subservices, s.ringLifecycler) } if cfg.QOSEnabled { @@ -220,6 +220,8 @@ func (s *service) starting(ctx context.Context) error { IndexMetrics: s.indexMetrics, Features: s.features, QOSQueue: s.queue, + Ring: s.searchRing, + RingLifecycler: s.ringLifecycler, } server, err := NewResourceServer(serverOptions) if err != nil { @@ -254,12 +256,12 @@ func (s *service) starting(ctx context.Context) error { s.log.Info("waiting until resource server is JOINING in the ring") lfcCtx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - if err := ring.WaitInstanceState(lfcCtx, s.storageRing, s.lifecycler.GetInstanceID(), ring.JOINING); err != nil { + if err := ring.WaitInstanceState(lfcCtx, s.searchRing, s.ringLifecycler.GetInstanceID(), ring.JOINING); err != nil { return fmt.Errorf("error switching to JOINING in the ring: %s", err) } s.log.Info("resource server is JOINING in the ring") - if err := s.lifecycler.ChangeState(ctx, ring.ACTIVE); err != nil { + if err := s.ringLifecycler.ChangeState(ctx, ring.ACTIVE); err != nil { return fmt.Errorf("error switching to ACTIVE in the ring: %s", err) } s.log.Info("resource server is ACTIVE in the ring") From ed9ef9e5fcecc26b1ebe465d20fbec082794d651 Mon Sep 17 00:00:00 2001 From: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:58:24 -0600 Subject: [PATCH 036/101] Chore: Update supported version docs (#108557) baldm0mma/ update versions --- docs/sources/upgrade-guide/when-to-upgrade/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/sources/upgrade-guide/when-to-upgrade/index.md b/docs/sources/upgrade-guide/when-to-upgrade/index.md index bd0ac788b43..f759b26cbb8 100644 --- a/docs/sources/upgrade-guide/when-to-upgrade/index.md +++ b/docs/sources/upgrade-guide/when-to-upgrade/index.md @@ -46,7 +46,7 @@ We provide release documentation in multiple places to address different needs: ## When to expect releases -Currently, Grafana is on a monthly release cycle. Here’s a look at scheduled releases for 2025: +Grafana currently follows a monthly release schedule. Below are the planned releases for 2025, though these dates may be subject to change: | **Release date** | **Grafana versions** | **Release type** | | ---------------- | ------------------------- | ---------------- | @@ -110,14 +110,14 @@ Here is an overview of version support through 2026: | 11.0.x | May 14, 2024 | February 14, 2025 | Not Supported | | 11.1.x | June 25, 2024 | April 23, 2025 | Not Supported | | 11.2.x | August 27, 2024 | May 27, 2025 | Not Supported | -| 11.3.x | October 22, 2024 | July 22, 2025 | Security & Critical Bugs Only | +| 11.3.x | October 22, 2024 | July 22, 2025 | Not Supported | | 11.4.x | December 5, 2024 | September 5, 2025 | Security & Critical Bugs Only | | 11.5.x | January 28, 2025 | October 28, 2025 | Security & Critical Bugs Only | | 11.6.x (Last minor of 11) | March 25, 2025 | May 25, 2026 | Security & Critical Bugs Only | -| 12.0.x | May 5, 2025 | February 5, 2026 | Full Support until next minor | +| 12.0.x | May 5, 2025 | February 5, 2026 | Security & Critical Bugs Only | | 12.1.x | July 22, 2025 | April 22, 2026 | Full Support until next minor | -| 12.2.x | September 23, 2025 | June 23, 2026 | Full Support until next minor | -| 12.3.x | November 18, 2025 | August 18, 2026 | Full Support until next minor | +| 12.2.x | September 23, 2025 | June 23, 2026 | Yet to be released | +| 12.3.x | November 18, 2025 | August 18, 2026 | Yet to be released | ## How are these versions supported? From 5bfed408edcc6f74dd8b0780b4f87bccfea02b01 Mon Sep 17 00:00:00 2001 From: Kristina Date: Wed, 23 Jul 2025 16:49:58 -0500 Subject: [PATCH 037/101] SQL Expressions: Enable Auto-complete (#106511) * First draft autocomplete * Better naming * Change suggestion to table/column population * Remove all suggestions, just use table/column population but trigger on space * Gate feature flag behind a feature flag * Reorganize and add function list * Add blurb about autocomplete to docs * Update public/app/features/expressions/utils/metaSqlExpr.ts Co-authored-by: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> * Add try catch, remove promise resolve --------- Co-authored-by: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> --- .../sql-expressions/index.md | 1 + .../src/types/featureToggles.gen.ts | 4 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 13 ++ .../components/expressions/Expression.tsx | 10 +- .../expressions/ExpressionDatasource.ts | 48 +++++- .../expressions/ExpressionQueryEditor.tsx | 2 +- .../expressions/components/SqlExpr.test.tsx | 6 +- .../expressions/components/SqlExpr.tsx | 39 +++-- .../components/sqlCompletionProvider.ts | 46 ++++++ .../features/expressions/utils/metaSqlExpr.ts | 155 ++++++++++++++++++ 13 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 public/app/features/expressions/components/sqlCompletionProvider.ts create mode 100644 public/app/features/expressions/utils/metaSqlExpr.ts diff --git a/docs/sources/panels-visualizations/query-transform-data/sql-expressions/index.md b/docs/sources/panels-visualizations/query-transform-data/sql-expressions/index.md index 7d891f8e9b6..629eaece47f 100644 --- a/docs/sources/panels-visualizations/query-transform-data/sql-expressions/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/sql-expressions/index.md @@ -150,6 +150,7 @@ The SQL conversion path: - Currently, only one SQL expression is supported per panel or alert. - Grafana supports certain data sources. Refer to [compatible data sources](#compatible-data-sources) for a current list. +- Autocomplete is available, but column/field autocomplete is only available after enabling the `sqlExpressionsColumnAutoComplete` feature toggle, which is provided on an experimental basis. ## Supported data source formats diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 9582065043f..3316e91792d 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -477,6 +477,10 @@ export interface FeatureToggles { */ sqlExpressions?: boolean; /** + * Enables column autocomplete for SQL Expressions + */ + sqlExpressionsColumnAutoComplete?: boolean; + /** * Enables the group to nested table transformation * @default true */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a5881618107..8958d488a3c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -803,6 +803,13 @@ var ( FrontendOnly: false, Owner: grafanaDatasourcesCoreServicesSquad, }, + { + Name: "sqlExpressionsColumnAutoComplete", + Description: "Enables column autocomplete for SQL Expressions", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDataProSquad, + }, { Name: "groupToNestedTableTransformation", Description: "Enables the group to nested table transformation", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 8bf9cca2706..b4cc812bf54 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -105,6 +105,7 @@ scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false promQLScope,GA,@grafana/oss-big-tent,false,false,false logQLScope,privatePreview,@grafana/observability-logs,false,false,false sqlExpressions,privatePreview,@grafana/grafana-datasources-core-services,false,false,false +sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true groupToNestedTableTransformation,GA,@grafana/dataviz-squad,false,false,true newPDFRendering,GA,@grafana/grafana-operator-experience-squad,false,false,false tlsMemcached,GA,@grafana/grafana-operator-experience-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 5e8ccbb5615..43a51188253 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -431,6 +431,10 @@ const ( // Enables SQL Expressions, which can execute SQL queries against data source results. FlagSqlExpressions = "sqlExpressions" + // FlagSqlExpressionsColumnAutoComplete + // Enables column autocomplete for SQL Expressions + FlagSqlExpressionsColumnAutoComplete = "sqlExpressionsColumnAutoComplete" + // FlagGroupToNestedTableTransformation // Enables the group to nested table transformation FlagGroupToNestedTableTransformation = "groupToNestedTableTransformation" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 88ab6c38de0..c1915403da5 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3111,6 +3111,19 @@ "codeowner": "@grafana/grafana-datasources-core-services" } }, + { + "metadata": { + "name": "sqlExpressionsColumnAutoComplete", + "resourceVersion": "1751471729972", + "creationTimestamp": "2025-07-02T15:55:29Z" + }, + "spec": { + "description": "Enables column autocomplete for SQL Expressions", + "stage": "experimental", + "codeowner": "@grafana/datapro", + "frontend": true + } + }, { "metadata": { "name": "sseGroupByDatasource", diff --git a/public/app/features/alerting/unified/components/expressions/Expression.tsx b/public/app/features/alerting/unified/components/expressions/Expression.tsx index 9b3d6a2a289..032feeabd5d 100644 --- a/public/app/features/alerting/unified/components/expressions/Expression.tsx +++ b/public/app/features/alerting/unified/components/expressions/Expression.tsx @@ -133,7 +133,15 @@ export const Expression: FC = ({ ); case ExpressionQueryType.sql: - return onChangeQuery(query)} query={query} refIds={availableRefIds} alerting />; + return ( + onChangeQuery(query)} + query={query} + refIds={availableRefIds} + alerting + queries={[]} + /> + ); default: return ( diff --git a/public/app/features/expressions/ExpressionDatasource.ts b/public/app/features/expressions/ExpressionDatasource.ts index 57f4b45ffbf..ded8c75d89f 100644 --- a/public/app/features/expressions/ExpressionDatasource.ts +++ b/public/app/features/expressions/ExpressionDatasource.ts @@ -1,15 +1,27 @@ -import { from, mergeMap, Observable } from 'rxjs'; +import { from, lastValueFrom, map, mergeMap, Observable } from 'rxjs'; import { + DataFrame, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, DataSourcePluginMeta, PluginType, ScopedVars, + TimeRange, } from '@grafana/data'; -import { DataSourceWithBackend, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime'; +import { SQLQuery } from '@grafana/plugin-ui'; +import { + BackendDataSourceResponse, + DataSourceWithBackend, + FetchResponse, + getBackendSrv, + getDataSourceSrv, + getTemplateSrv, + toDataQueryResponse, +} from '@grafana/runtime'; import { ExpressionDatasourceRef } from '@grafana/runtime/internal'; +import { DataQuery } from '@grafana/schema/dist/esm/index'; import icnDatasourceSvg from 'img/icn-datasource.svg'; import { ExpressionQueryEditor } from './ExpressionQueryEditor'; @@ -59,6 +71,38 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend, range: TimeRange, queries: DataQuery[]): Promise { + const refId = request.refId || 'meta'; + const metaSqlExpressionQuery: ExpressionQuery = { + window: '', + hide: false, + expression: request.rawSql, + datasource: ExpressionDatasourceRef, + refId, + type: ExpressionQueryType.sql, + }; + return lastValueFrom( + getBackendSrv() + .fetch({ + url: '/api/ds/query', + method: 'POST', + headers: this.getRequestHeaders(), + data: { + from: range.from.valueOf().toString(), + to: range.to.valueOf().toString(), + queries: [...queries, metaSqlExpressionQuery], + }, + requestId: refId, + }) + .pipe( + map((res: FetchResponse) => { + const rsp = toDataQueryResponse(res, queries); + return rsp.data[0] ?? { fields: [] }; + }) + ) + ); + } } export const instanceSettings: DataSourceInstanceSettings = { diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx index b1f07c253bb..e49d3932c48 100644 --- a/public/app/features/expressions/ExpressionQueryEditor.tsx +++ b/public/app/features/expressions/ExpressionQueryEditor.tsx @@ -118,7 +118,7 @@ export function ExpressionQueryEditor(props: Props) { return ; case ExpressionQueryType.sql: - return ; + return ; } }; diff --git a/public/app/features/expressions/components/SqlExpr.test.tsx b/public/app/features/expressions/components/SqlExpr.test.tsx index 566e180fabf..e63a80133d6 100644 --- a/public/app/features/expressions/components/SqlExpr.test.tsx +++ b/public/app/features/expressions/components/SqlExpr.test.tsx @@ -19,7 +19,7 @@ describe('SqlExpr', () => { const refIds = [{ value: 'A' }]; const query = { refId: 'expr1', type: 'sql', expression: '' } as ExpressionQuery; - render(); + render(); // Verify onChange was called expect(onChange).toHaveBeenCalled(); @@ -35,7 +35,7 @@ describe('SqlExpr', () => { const existingExpression = 'SELECT 1 AS foo'; const query = { refId: 'expr1', type: 'sql', expression: existingExpression } as ExpressionQuery; - render(); + render(); // Check if onChange was called if (onChange.mock.calls.length > 0) { @@ -53,7 +53,7 @@ describe('SqlExpr', () => { const refIds = [{ value: 'A' }]; const query = { refId: 'expr1', type: 'sql' } as ExpressionQuery; - render(); + render(); const updatedQuery = onChange.mock.calls[0][0]; expect(updatedQuery.format).toBe('alerting'); diff --git a/public/app/features/expressions/components/SqlExpr.tsx b/public/app/features/expressions/components/SqlExpr.tsx index 1f61a558471..ac2917b1c40 100644 --- a/public/app/features/expressions/components/SqlExpr.tsx +++ b/public/app/features/expressions/components/SqlExpr.tsx @@ -2,34 +2,44 @@ import { css } from '@emotion/css'; import { useMemo, useRef, useEffect, useState } from 'react'; import { SelectableValue } from '@grafana/data'; -import { SQLEditor, LanguageDefinition } from '@grafana/plugin-ui'; +import { SQLEditor, CompletionItemKind, LanguageDefinition, TableIdentifier } from '@grafana/plugin-ui'; +import { DataQuery } from '@grafana/schema/dist/esm/index'; import { useStyles2 } from '@grafana/ui'; import { SqlExpressionQuery } from '../types'; +import { fetchSQLFields } from '../utils/metaSqlExpr'; + +import { getSqlCompletionProvider } from './sqlCompletionProvider'; // Account for Monaco editor's border to prevent clipping const EDITOR_BORDER_ADJUSTMENT = 2; // 1px border on top and bottom -// Define the language definition for MySQL syntax highlighting and autocomplete -const EDITOR_LANGUAGE_DEFINITION: LanguageDefinition = { - id: 'mysql', - // Additional properties could be added here in the future if needed - // eg: - // completionProvider: to autocomplete field (ie column) names when given - // a table name (dataframe reference) - // formatter: to format the SQL query and dashboard variables -}; - interface Props { refIds: Array>; query: SqlExpressionQuery; + queries: DataQuery[] | undefined; onChange: (query: SqlExpressionQuery) => void; /** Should the `format` property be set to `alerting`? */ alerting?: boolean; } -export const SqlExpr = ({ onChange, refIds, query, alerting = false }: Props) => { +export const SqlExpr = ({ onChange, refIds, query, alerting = false, queries }: Props) => { const vars = useMemo(() => refIds.map((v) => v.value!), [refIds]); + const completionProvider = useMemo( + () => + getSqlCompletionProvider({ + getFields: (identifier: TableIdentifier) => fetchFields(identifier, queries || []), + refIds, + }), + [queries, refIds] + ); + + // Define the language definition for MySQL syntax highlighting and autocomplete + const EDITOR_LANGUAGE_DEFINITION: LanguageDefinition = { + id: 'mysql', + completionProvider, + }; + const initialQuery = `SELECT * FROM ${vars[0]} LIMIT 10`; @@ -90,3 +100,8 @@ const getStyles = () => ({ minHeight: '100px', }), }); + +async function fetchFields(identifier: TableIdentifier, queries: DataQuery[]) { + const fields = await fetchSQLFields({ table: identifier.table }, queries); + return fields.map((t) => ({ name: t.name, completion: t.value, kind: CompletionItemKind.Field })); +} diff --git a/public/app/features/expressions/components/sqlCompletionProvider.ts b/public/app/features/expressions/components/sqlCompletionProvider.ts new file mode 100644 index 00000000000..013088aa770 --- /dev/null +++ b/public/app/features/expressions/components/sqlCompletionProvider.ts @@ -0,0 +1,46 @@ +import { SelectableValue } from '@grafana/data'; +import { ColumnDefinition, LanguageCompletionProvider, TableDefinition, TableIdentifier } from '@grafana/plugin-ui'; +import { config } from '@grafana/runtime'; + +import { ALLOWED_FUNCTIONS } from '../utils/metaSqlExpr'; + +interface CompletionProviderGetterArgs { + getFields: (t: TableIdentifier) => Promise; + refIds: Array>; +} + +export const getSqlCompletionProvider: (args: CompletionProviderGetterArgs) => LanguageCompletionProvider = + (args) => (monaco, language) => ({ + ...language, + triggerCharacters: [' '], + tables: { + resolve: async () => { + const refIdsToTableDefs = args.refIds.map((refId) => { + const tableDef: TableDefinition = { + name: refId.label || refId.value || '', + completion: refId.label || refId.value || '', + }; + return tableDef; + }); + return refIdsToTableDefs; + }, + }, + columns: { + resolve: async (t?: TableIdentifier) => { + if (config.featureToggles.sqlExpressionsColumnAutoComplete) { + try { + return await args.getFields({ table: t?.table }); + } catch { + return []; + } + } else { + return []; + } + }, + }, + supportedFunctions: () => { + return ALLOWED_FUNCTIONS.map((func) => { + return { id: func, name: func }; + }); + }, + }); diff --git a/public/app/features/expressions/utils/metaSqlExpr.ts b/public/app/features/expressions/utils/metaSqlExpr.ts new file mode 100644 index 00000000000..349d2abb8a3 --- /dev/null +++ b/public/app/features/expressions/utils/metaSqlExpr.ts @@ -0,0 +1,155 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { getDefaultTimeRange, DataFrameView } from '@grafana/data'; +import { QueryFormat, SQLQuery, SQLSelectableValue } from '@grafana/plugin-ui'; +import { DataQuery } from '@grafana/schema'; +import { mapFieldsToTypes } from 'app/plugins/datasource/mysql/fields'; +import { quoteIdentifierIfNecessary } from 'app/plugins/datasource/mysql/sqlUtil'; + +import { dataSource } from '../ExpressionDatasource'; + +export async function fetchSQLFields(query: Partial, queries: DataQuery[]): Promise { + const datasource = dataSource; + if (!query.table) { + return []; + } + + const queryString = `SELECT * FROM ${query.table} LIMIT 1`; + + const queryResponse = await datasource.runMetaSQLExprQuery( + { rawSql: queryString, format: QueryFormat.Table, refId: `fields-${uuidv4()}` }, + getDefaultTimeRange(), + queries.filter((q) => q.refId === query.table) + ); + const frame = new DataFrameView(queryResponse); + + const fields = Object.values(frame.fields).map(({ name, type }) => { + return { + name, + text: name, + label: name, + value: quoteIdentifierIfNecessary(name), + type, + }; + }); + + return mapFieldsToTypes(fields); +} + +// based off https://github.com/grafana/grafana/blob/main/pkg/expr/sql/parser_allow.go +export const ALLOWED_FUNCTIONS = [ + 'if', + 'coalesce', + 'ifnull', + 'nullif', + 'sum', + 'avg', + 'count', + 'min', + 'max', + 'stddev', + 'std', + 'stddev_pop', + 'variance', + 'var_pop', + 'group_concat', + 'row_number', + 'rank', + 'dense_rank', + 'lead', + 'lag', + 'first_value', + 'last_value', + 'abs', + 'round', + 'floor', + 'ceiling', + 'ceil', + 'sqrt', + 'pow', + 'power', + 'mod', + 'log', + 'log10', + 'exp', + 'sign', + 'ln', + 'truncate', + 'sin', + 'cos', + 'tan', + 'asin', + 'acos', + 'atan', + 'atan2', + 'rand', + 'pi', + 'concat', + 'length', + 'char_length', + 'lower', + 'upper', + 'substring', + 'substring_index', + 'left', + 'right', + 'ltrim', + 'rtrim', + 'replace', + 'reverse', + 'lcase', + 'ucase', + 'mid', + 'repeat', + 'position', + 'instr', + 'locate', + 'ascii', + 'ord', + 'char', + 'regexp_substr', + 'str_to_date', + 'date_format', + 'date_add', + 'date_sub', + 'year', + 'month', + 'day', + 'weekday', + 'datediff', + 'unix_timestamp', + 'from_unixtime', + 'extract', + 'hour', + 'minute', + 'second', + 'dayname', + 'monthname', + 'dayofweek', + 'dayofmonth', + 'dayofyear', + 'week', + 'quarter', + 'time_to_sec', + 'sec_to_time', + 'timestampdiff', + 'timestampadd', + 'cast', + 'convert', + 'json_extract', + 'json_object', + 'json_array', + 'json_merge_patch', + 'json_valid', + 'json_contains', + 'json_length', + 'json_type', + 'json_keys', + 'json_search', + 'json_quote', + 'json_unquote', + 'json_set', + 'json_insert', + 'json_replace', + 'json_remove', +]; From 63cc23a3ed55c265012ff7288e95f9e5992e9b29 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 23 Jul 2025 18:06:31 -0400 Subject: [PATCH 038/101] TableNG: Footer should render summary value in first column if present (#108550) * TableNG: Footer should render summary value in first column if present * fix issue where empty fields array meant no calcs were shown * fix a bug related to hidden fields * go away from fieldmatcher to use a simple inline solution --- .../src/components/Table/TableNG/TableNG.tsx | 18 +++++++--- .../components/Table/TableNG/hooks.test.ts | 33 +++++++++++------ .../src/components/Table/TableNG/hooks.ts | 36 ++++++++----------- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 8c4e1e62959..c84fb2d4194 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -210,7 +210,11 @@ export function TableNG(props: TableNGProps) { }); // Create a map of column key to text wrap - const footerCalcs = useFooterCalcs(sortedRows, data, { enabled: hasFooter, footerOptions, isCountRowsSet }); + const footerCalcs = useFooterCalcs(sortedRows, visibleFields, { + enabled: hasFooter, + footerOptions, + isCountRowsSet, + }); const applyToRowBgFn = useMemo(() => getApplyToRowBgFn(data.fields, theme) ?? undefined, [data.fields, theme]); const renderRow = useMemo( @@ -791,6 +795,8 @@ const getGridStyles = ( '--rdg-header-background-color': transparent ? theme.colors.background.canvas : theme.colors.background.primary, '--rdg-border-color': theme.colors.border.weak, '--rdg-color': theme.colors.text.primary, + '--rdg-summary-border-color': theme.colors.border.weak, + '--rdg-summary-border-width': '1px', // note: this cannot have any transparency since default cells that // overlay/overflow on hover inherit this background and need to occlude cells below @@ -807,6 +813,12 @@ const getGridStyles = ( border: 'none', + '.rdg-cell': { + '&:last-child': { + borderInlineEnd: 'none', + }, + }, + // add a box shadow on hover and selection for all body cells '& > :not(.rdg-summary-row, .rdg-header-row) > .rdg-cell': { '&:hover, &[aria-selected=true]': { @@ -941,10 +953,6 @@ const getCellStyles = ( ...(shouldWrap && { whiteSpace: isMonospace ? 'pre' : 'pre-line' }), ...(isMonospace && { fontFamily: 'monospace' }), - '&:last-child': { - borderInlineEnd: 'none', - }, - // should omit if no cell actions, and no shouldOverflow '&:hover, &[aria-selected=true]': { '.table-cell-actions': { diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts index afbc4fff691..4df9128205a 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.test.ts @@ -288,7 +288,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, numericField] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: { show: true, reducer: ['sum'] }, }); @@ -301,7 +301,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, numericField] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: { show: true, reducer: ['mean'] }, }); @@ -314,7 +314,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, textField] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: { show: true, reducer: ['sum'] }, }); @@ -327,7 +327,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, numericField, numericField2] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: undefined, }); @@ -340,7 +340,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, numericField, numericField2] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: false, footerOptions: { show: true, reducer: ['sum'] }, }); @@ -353,7 +353,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, textField] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: { show: true, reducer: undefined }, }); @@ -366,7 +366,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, numericField, numericField2] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: { show: true, reducer: [] }, }); @@ -379,7 +379,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, numericField, numericField2] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: { show: true, reducer: ['sum'], fields: ['Field2', 'Field3'] }, }); @@ -392,7 +392,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, numericField, numericField2] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field2', 'Field3'] }, }); @@ -405,7 +405,7 @@ describe('TableNG hooks', () => { const { result } = renderHook(() => { const data = createDataFrame({ fields: [textField, numericField, numericField2] }); cacheFieldDisplayNames([data]); - return useFooterCalcs(rows, data, { + return useFooterCalcs(rows, data.fields, { enabled: true, footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field 2'] }, }); @@ -413,6 +413,19 @@ describe('TableNG hooks', () => { expect(result.current).toEqual(['Total', '6', '13']); }); + + it('should not return the reducer label in the first column if there is a calc to render', () => { + const { result } = renderHook(() => { + const data = createDataFrame({ fields: [numericField, numericField2] }); + cacheFieldDisplayNames([data]); + return useFooterCalcs(rows, data.fields, { + enabled: true, + footerOptions: { show: true, reducer: ['sum'], fields: [] }, + }); + }); + + expect(result.current).toEqual(['6', '13']); + }); }); describe('useHeaderHeight', () => { diff --git a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts index a11be3d165a..e5f30a68d27 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/hooks.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/hooks.ts @@ -2,16 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect, Ref import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid'; import { varPreLine } from 'uwrap'; -import { - DataFrame, - Field, - FieldMatcherID, - fieldReducers, - FieldType, - formattedValueToString, - getFieldMatcher, - reduceField, -} from '@grafana/data'; +import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data'; import { useTheme2 } from '../../../themes/ThemeContext'; import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types'; @@ -264,7 +255,8 @@ export interface FooterCalcsOptions { export function useFooterCalcs( rows: TableRow[], - data: DataFrame, + // it's very important that this is the _visible_ fields. + fields: Field[], { enabled, footerOptions, isCountRowsSet }: FooterCalcsOptions ): string[] { return useMemo(() => { @@ -274,11 +266,9 @@ export function useFooterCalcs( return []; } - const fieldNameMatcher = footerOptions.fields - ? getFieldMatcher({ id: FieldMatcherID.byNames, options: { names: footerOptions.fields } }) - : undefined; + const fieldNameSet = footerOptions.fields?.length ? new Set(footerOptions.fields) : null; - return data.fields.map((field, index) => { + return fields.map((field, index) => { if (field.state?.calcs) { delete field.state?.calcs; } @@ -287,25 +277,27 @@ export function useFooterCalcs( return index === 0 ? `${rows.length}` : ''; } + let emptyValue = ''; if (index === 0) { const footerCalcReducer = footerReducers[0]; - return footerCalcReducer ? fieldReducers.get(footerCalcReducer).name : ''; + emptyValue = footerCalcReducer ? fieldReducers.get(footerCalcReducer).name : ''; } if (field.type !== FieldType.number) { - return ''; + return emptyValue; } // if field.display is undefined, don't throw const displayFn = field.display; if (!displayFn) { - return ''; + return emptyValue; } // If fields array is specified, only show footer for fields included in that array. - // the array can include either the display name or the field name. - if (fieldNameMatcher && !fieldNameMatcher(field, data, [data])) { - return ''; + // the array can include either the display name or the field name. we don't use a field matcher + // because that requires us to drill the data frame down here. + if (fieldNameSet && !fieldNameSet.has(getDisplayName(field)) && !fieldNameSet.has(field.name)) { + return emptyValue; } const calc = footerReducers[0]; @@ -319,7 +311,7 @@ export function useFooterCalcs( return formattedValueToString(displayFn(value)); }); - }, [data, enabled, footerOptions, isCountRowsSet, rows]); + }, [fields, enabled, footerOptions, isCountRowsSet, rows]); } interface TypographyCtx { From 95eb66be3d1c272e19bcaf522bce7aecae8689ba Mon Sep 17 00:00:00 2001 From: Kristina Date: Wed, 23 Jul 2025 17:41:27 -0500 Subject: [PATCH 039/101] Transformations: Extract Fields > Regex should not encode tooltip text (#108410) do not escape tooltip text --- .../extractFields/ExtractFieldsTransformerEditor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx index f4d1773b8f2..1d458d988bd 100644 --- a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx @@ -127,6 +127,7 @@ export const extractFieldsTransformerEditor = ({ interactive={true} tooltip={t('transformers.extract-fields-transformer-editor.tooltip-regexp', 'Example: {{regexExample}}', { regexExample: '/(?.*)/', + interpolation: { escapeValue: false }, })} > Date: Thu, 24 Jul 2025 00:45:05 +0000 Subject: [PATCH 040/101] I18n: Download translations from Crowdin (#108561) 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 | 17 ++++++++++++++--- public/locales/de-DE/grafana.json | 17 ++++++++++++++--- public/locales/es-ES/grafana.json | 17 ++++++++++++++--- public/locales/fr-FR/grafana.json | 17 ++++++++++++++--- public/locales/hu-HU/grafana.json | 17 ++++++++++++++--- public/locales/id-ID/grafana.json | 17 ++++++++++++++--- public/locales/it-IT/grafana.json | 17 ++++++++++++++--- public/locales/ja-JP/grafana.json | 17 ++++++++++++++--- public/locales/ko-KR/grafana.json | 17 ++++++++++++++--- public/locales/nl-NL/grafana.json | 17 ++++++++++++++--- public/locales/pl-PL/grafana.json | 17 ++++++++++++++--- public/locales/pt-BR/grafana.json | 17 ++++++++++++++--- public/locales/pt-PT/grafana.json | 17 ++++++++++++++--- public/locales/ru-RU/grafana.json | 17 ++++++++++++++--- public/locales/sv-SE/grafana.json | 17 ++++++++++++++--- public/locales/tr-TR/grafana.json | 17 ++++++++++++++--- public/locales/zh-Hans/grafana.json | 17 ++++++++++++++--- public/locales/zh-Hant/grafana.json | 17 ++++++++++++++--- 18 files changed, 252 insertions(+), 54 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index c114c5f82e6..0b6722816ae 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1563,7 +1564,8 @@ "title": "Upravit hodnotící skupinu" }, "group-loader": { - "group-load-failed": "Nepodařilo se načíst pravidla ze skupiny {{ groupName }} v jmenném prostoru {{ namespaceName }}" + "group-load-failed": "Nepodařilo se načíst pravidla ze skupiny {{ groupName }} v jmenném prostoru {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Probíhá odstranění skupiny" @@ -2828,6 +2830,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Obnovit náhled", + "empty-state": "", "label-preview": "Náhled", "refresh": "Obnovit" }, @@ -8641,7 +8644,10 @@ "no-data": "" }, "no-values-label": "Žádné hodnoty", - "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} z {{numRows}} řádků" + "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} z {{numRows}} řádků", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Tagy" @@ -9390,6 +9396,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "Žádná pole k zobrazení.", + "open-assistant": "", "pin-line": "Připnout protokol", "remove-displayed-field": "", "remove-log": "", @@ -9408,6 +9415,9 @@ "copy-log": "Kopírovat řádek protokolu", "hide-details": "Zobrazit podrobnosti protokolu", "icon-label": "Nabídka protokolu", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Připnout protokol", "show-context": "Zobrazit kontext", "show-details": "Skrýt podrobnosti protokolu", @@ -11279,7 +11289,8 @@ "starting": "Spouštění…", "status": { "title-error-running-job": "Chyba při spouštění úlohy", - "title-job-completed-successfully": "Úloha byla úspěšně dokončena" + "title-job-completed-successfully": "Úloha byla úspěšně dokončena", + "title-warning-running-job": "" }, "summary": "Shrnutí" }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 34931789e34..b3056eae2d5 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Evaluierungsgruppe bearbeiten" }, "group-loader": { - "group-load-failed": "Die Regeln der Gruppe {{ groupName }} in {{ namespaceName }} konnten nicht geladen werden" + "group-load-failed": "Die Regeln der Gruppe {{ groupName }} in {{ namespaceName }} konnten nicht geladen werden", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Die Gruppe wird gelöscht" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Vorschau aktualisieren", + "empty-state": "", "label-preview": "Vorschau", "refresh": "Aktualisieren" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Keine Werte", - "pagination-summary": "{{itemsRangeStart}}–{{displayedEnd}} von {{numRows}} Zeilen" + "pagination-summary": "{{itemsRangeStart}}–{{displayedEnd}} von {{numRows}} Zeilen", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Tags" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "Keine anzuzeigenden Felder.", + "open-assistant": "", "pin-line": "Log anheften", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Protokollzeile kopieren", "hide-details": "Logdetails anzeigen", "icon-label": "Protokollmenü", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Protokoll anheften", "show-context": "Kontext anzeigen", "show-details": "Logdetails ausblenden", @@ -11207,7 +11217,8 @@ "starting": "Wird gestartet ...", "status": { "title-error-running-job": "Fehler bei der Ausführung des Auftrags", - "title-job-completed-successfully": "Auftrag erfolgreich abgeschlossen" + "title-job-completed-successfully": "Auftrag erfolgreich abgeschlossen", + "title-warning-running-job": "" }, "summary": "Zusammenfassung" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 2512bd3db7f..3fd26a8acaf 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Editar grupo de evaluación" }, "group-loader": { - "group-load-failed": "Error al cargar las reglas del grupo {{ groupName }} en {{ namespaceName }}" + "group-load-failed": "Error al cargar las reglas del grupo {{ groupName }} en {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "El grupo se está eliminando" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Actualizar vista previa", + "empty-state": "", "label-preview": "Vista previa", "refresh": "Actualizar" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Sin valores", - "pagination-summary": "{{itemsRangeStart}}-{{displayedEnd}} de {{numRows}} filas" + "pagination-summary": "{{itemsRangeStart}}-{{displayedEnd}} de {{numRows}} filas", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Etiquetas" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Copiar línea de registro", "hide-details": "Mostrar detalles del log", "icon-label": "Menú de registro", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Anclar registro", "show-context": "Mostrar contexto", "show-details": "Ocultar detalles del log", @@ -11207,7 +11217,8 @@ "starting": "Empezando...", "status": { "title-error-running-job": "Error al ejecutar el trabajo", - "title-job-completed-successfully": "Trabajo completado con éxito" + "title-job-completed-successfully": "Trabajo completado con éxito", + "title-warning-running-job": "" }, "summary": "Resumen" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index b413dec5dff..6278e48a1d8 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Modifier le groupe d’évaluation" }, "group-loader": { - "group-load-failed": "Échec du chargement des règles du groupe {{ groupName }} dans {{ namespaceName }}" + "group-load-failed": "Échec du chargement des règles du groupe {{ groupName }} dans {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Le groupe est en cours de suppression" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Actualiser l’aperçu", + "empty-state": "", "label-preview": "Aperçu", "refresh": "Actualiser" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Aucune valeur", - "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} sur {{numRows}} lignes" + "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} sur {{numRows}} lignes", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Balises" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "Aucun champ à afficher.", + "open-assistant": "", "pin-line": "Épingler ce log", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Copier la ligne de journal", "hide-details": "Afficher les détails du journal", "icon-label": "Menu du journal", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Épingler le journal", "show-context": "Afficher le contexte", "show-details": "Masquer les détails du journal", @@ -11207,7 +11217,8 @@ "starting": "Démarrage...", "status": { "title-error-running-job": "Erreur lors de l’exécution de la mission", - "title-job-completed-successfully": "Mission accomplie" + "title-job-completed-successfully": "Mission accomplie", + "title-warning-running-job": "" }, "summary": "Synthèse" }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 4ef8f071309..7d45de61805 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Értékelési csoport szerkesztése" }, "group-loader": { - "group-load-failed": "A szabályok betöltése a(z) {{ groupName }} csoportból nem sikerült a(z) {{ namespaceName }} névtérben" + "group-load-failed": "A szabályok betöltése a(z) {{ groupName }} csoportból nem sikerült a(z) {{ namespaceName }} névtérben", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "A csoport törlése folyamatban van" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Előnézet frissítése", + "empty-state": "", "label-preview": "Előnézet", "refresh": "Frissítés" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Nincsenek értékek", - "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} / {{numRows}} sor" + "pagination-summary": "{{itemsRangeStart}} – {{displayedEnd}} / {{numRows}} sor", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Címkék" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "Nincs megjeleníthető mező.", + "open-assistant": "", "pin-line": "Napló kitűzése", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Naplósor másolása", "hide-details": "Napló részleteinek megjelenítése", "icon-label": "Naplómenü", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Napló rögzítése", "show-context": "Kontextus megjelenítése", "show-details": "Napló részleteinek elrejtése", @@ -11207,7 +11217,8 @@ "starting": "Kezdés…", "status": { "title-error-running-job": "Hiba történt a feladat futtatásakor", - "title-job-completed-successfully": "A feladat sikeresen befejeződött" + "title-job-completed-successfully": "A feladat sikeresen befejeződött", + "title-warning-running-job": "" }, "summary": "Összegzés" }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index aede0df0441..d1a2a2ae3bd 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1545,7 +1546,8 @@ "title": "Edit grup evaluasi" }, "group-loader": { - "group-load-failed": "Gagal memuat aturan dari grup {{ groupName }} di {{ namespaceName }}" + "group-load-failed": "Gagal memuat aturan dari grup {{ groupName }} di {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Grup sedang dihapus" @@ -2789,6 +2791,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Refresh pratinjau", + "empty-state": "", "label-preview": "Pratinjau", "refresh": "Muat ulang" }, @@ -8575,7 +8578,10 @@ "no-data": "" }, "no-values-label": "Tidak ada nilai", - "pagination-summary": "{{itemsRangeStart}}—{{displayedEnd}} dari {{numRows}} baris" + "pagination-summary": "{{itemsRangeStart}}—{{displayedEnd}} dari {{numRows}} baris", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Tag" @@ -9300,6 +9306,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "Tidak ada bidang untuk ditampilkan.", + "open-assistant": "", "pin-line": "Sematkan log", "remove-displayed-field": "", "remove-log": "", @@ -9318,6 +9325,9 @@ "copy-log": "Salin baris log", "hide-details": "Tampilkan detail log", "icon-label": "Menu log", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Sematkan log", "show-context": "Tampilkan konteks", "show-details": "Sembunyikan detail log", @@ -11171,7 +11181,8 @@ "starting": "Memulai...", "status": { "title-error-running-job": "Kesalahan saat menjalankan pekerjaan", - "title-job-completed-successfully": "Pekerjaan berhasil diselesaikan" + "title-job-completed-successfully": "Pekerjaan berhasil diselesaikan", + "title-warning-running-job": "" }, "summary": "Ringkasan" }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 268d227ce05..46784bccbba 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Modifica il gruppo di valutazione" }, "group-loader": { - "group-load-failed": "Impossibile caricare le regole dal gruppo {{ groupName }} in {{ namespaceName }}" + "group-load-failed": "Impossibile caricare le regole dal gruppo {{ groupName }} in {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Il gruppo è in fase di eliminazione" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Aggiorna anteprima", + "empty-state": "", "label-preview": "Anteprima", "refresh": "Aggiorna" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Nessun valore", - "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} di {{numRows}} righe" + "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} di {{numRows}} righe", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Tag" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Copia la riga del registro", "hide-details": "Mostra i dettagli del registro", "icon-label": "Menu registro", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Fissa registro", "show-context": "Mostra contesto", "show-details": "Nascondi i dettagli del registro", @@ -11207,7 +11217,8 @@ "starting": "Avvio in corso...", "status": { "title-error-running-job": "Errore durante l'esecuzione dell'attività", - "title-job-completed-successfully": "Attività completata con successo" + "title-job-completed-successfully": "Attività completata con successo", + "title-warning-running-job": "" }, "summary": "Indice" }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index 1fa5b0e181e..d5b9ae47f12 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1545,7 +1546,8 @@ "title": "評価グループを編集" }, "group-loader": { - "group-load-failed": "{{ namespaceName }}の{{ groupName }}グループからルールを読み込めませんでした" + "group-load-failed": "{{ namespaceName }}の{{ groupName }}グループからルールを読み込めませんでした", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "グループを削除中です" @@ -2789,6 +2791,7 @@ }, "template-preview": { "aria-label-refresh-preview": "プレビューを更新", + "empty-state": "", "label-preview": "プレビュー", "refresh": "更新" }, @@ -8575,7 +8578,10 @@ "no-data": "" }, "no-values-label": "値がありません", - "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}}({{numRows}}行)" + "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}}({{numRows}}行)", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "タグ" @@ -9300,6 +9306,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "表示するフィールドがありません。", + "open-assistant": "", "pin-line": "ログをピン留め", "remove-displayed-field": "", "remove-log": "", @@ -9318,6 +9325,9 @@ "copy-log": "ログ行をコピー", "hide-details": "ログの詳細を表示", "icon-label": "ログメニュー", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "ログをピン留めする", "show-context": "コンテキストを表示", "show-details": "ログの詳細を非表示", @@ -11171,7 +11181,8 @@ "starting": "開始中...", "status": { "title-error-running-job": "実行エラー", - "title-job-completed-successfully": "ジョブが正常に完了しました" + "title-job-completed-successfully": "ジョブが正常に完了しました", + "title-warning-running-job": "" }, "summary": "概要" }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 7b75891fede..deeda946df1 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1545,7 +1546,8 @@ "title": "평가 그룹 편집" }, "group-loader": { - "group-load-failed": "{{ namespaceName }}의 {{ groupName }} 그룹에서 규칙 로딩 실패" + "group-load-failed": "{{ namespaceName }}의 {{ groupName }} 그룹에서 규칙 로딩 실패", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "그룹을 삭제하는 중입니다" @@ -2789,6 +2791,7 @@ }, "template-preview": { "aria-label-refresh-preview": "미리 보기 새로 고침", + "empty-state": "", "label-preview": "미리보기", "refresh": "새로 고침" }, @@ -8575,7 +8578,10 @@ "no-data": "" }, "no-values-label": "값 없음", - "pagination-summary": "{{numRows}}개의 행 중에서 {{itemsRangeStart}}~{{displayedEnd}}" + "pagination-summary": "{{numRows}}개의 행 중에서 {{itemsRangeStart}}~{{displayedEnd}}", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "태그" @@ -9300,6 +9306,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9318,6 +9325,9 @@ "copy-log": "로그 라인 복사", "hide-details": "로그 세부 정보 표시", "icon-label": "로그 메뉴", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "로그 고정", "show-context": "맥락 보기", "show-details": "로그 세부 정보 숨기기", @@ -11171,7 +11181,8 @@ "starting": "시작하는 중...", "status": { "title-error-running-job": "작업 실행 중 오류 발생", - "title-job-completed-successfully": "작업이 성공적으로 완료되었습니다" + "title-job-completed-successfully": "작업이 성공적으로 완료되었습니다", + "title-warning-running-job": "" }, "summary": "요약" }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index ee1f03f7e86..93b2b0337d5 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Evaluatiegroep bewerken" }, "group-loader": { - "group-load-failed": "Kan regels uit groep {{ groupName }} in {{ namespaceName }} niet laden" + "group-load-failed": "Kan regels uit groep {{ groupName }} in {{ namespaceName }} niet laden", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "De groep wordt verwijderd" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Voorbeeld vernieuwen", + "empty-state": "", "label-preview": "Voorbeeld", "refresh": "Vernieuwen" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Geen waarden", - "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} van {{numRows}} rijen" + "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} van {{numRows}} rijen", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Tags" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "Weer te geven velden.", + "open-assistant": "", "pin-line": "Logboek vastzetten", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Logboekregel kopiëren", "hide-details": "Logboekdetails weergeven", "icon-label": "Logboekmenu", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Logboek vastzetten", "show-context": "Context tonen", "show-details": "Logboekdetails verbergen", @@ -11207,7 +11217,8 @@ "starting": "Starten...", "status": { "title-error-running-job": "Er is een fout opgetreden bij het uitvoeren van de taak", - "title-job-completed-successfully": "Taak is voltooid" + "title-job-completed-successfully": "Taak is voltooid", + "title-warning-running-job": "" }, "summary": "Samenvatting" }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 776a6cb5c08..e41d5c348f8 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1563,7 +1564,8 @@ "title": "Edytuj grupę ocen" }, "group-loader": { - "group-load-failed": "Nie udało się wczytać reguł z grupy {{ groupName }} w przestrzeni nazw {{ namespaceName }}" + "group-load-failed": "Nie udało się wczytać reguł z grupy {{ groupName }} w przestrzeni nazw {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Trwa usuwanie grupy" @@ -2828,6 +2830,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Odśwież podgląd", + "empty-state": "", "label-preview": "Podgląd", "refresh": "Odśwież" }, @@ -8641,7 +8644,10 @@ "no-data": "" }, "no-values-label": "Brak wartości", - "pagination-summary": "{{itemsRangeStart}}–{{displayedEnd}} z {{numRows}} wierszy" + "pagination-summary": "{{itemsRangeStart}}–{{displayedEnd}} z {{numRows}} wierszy", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Znaczniki" @@ -9390,6 +9396,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9408,6 +9415,9 @@ "copy-log": "Kopiuj wiersz logu", "hide-details": "Pokaż szczegóły dziennika", "icon-label": "Menu logu", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Przypnij log", "show-context": "Pokaż kontekst", "show-details": "Ukryj szczegóły dziennika", @@ -11279,7 +11289,8 @@ "starting": "Uruchamianie…", "status": { "title-error-running-job": "Błąd uruchamiania zadania", - "title-job-completed-successfully": "Zadanie zakończone pomyślnie" + "title-job-completed-successfully": "Zadanie zakończone pomyślnie", + "title-warning-running-job": "" }, "summary": "Podsumowanie" }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 36500301b64..78144d213ba 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Editar grupo de avaliação" }, "group-loader": { - "group-load-failed": "Falha ao carregar regras do grupo {{ groupName }} em {{ namespaceName }}" + "group-load-failed": "Falha ao carregar regras do grupo {{ groupName }} em {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "O grupo está sendo excluído" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Atualizar a pré-visualização", + "empty-state": "", "label-preview": "Visualizar", "refresh": "Atualizar" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Sem valores", - "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} de {{numRows}} linhas" + "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} de {{numRows}} linhas", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Tags" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Copiar linha do log", "hide-details": "Exibir detalhes do log", "icon-label": "Menu do log", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Fixar log", "show-context": "Exibir contexto", "show-details": "Ocultar detalhes do log", @@ -11207,7 +11217,8 @@ "starting": "Iniciando…", "status": { "title-error-running-job": "Erro ao executar a tarefa", - "title-job-completed-successfully": "Tarefa concluída com sucesso" + "title-job-completed-successfully": "Tarefa concluída com sucesso", + "title-warning-running-job": "" }, "summary": "Resumo" }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index cc5e79646ef..4145a491e1f 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Editar grupo de avaliação" }, "group-loader": { - "group-load-failed": "Falha ao carregar regras do grupo {{ groupName }} em {{ namespaceName }}" + "group-load-failed": "Falha ao carregar regras do grupo {{ groupName }} em {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "O grupo está a ser eliminado" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Recarregar a pré-visualização", + "empty-state": "", "label-preview": "Pré-visualizar", "refresh": "Atualizar" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Nenhum valor", - "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} de {{numRows}} linhas" + "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} de {{numRows}} linhas", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Etiquetas" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Copiar linha de registo", "hide-details": "Mostrar detalhes do registo", "icon-label": "Menu de registo", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Afixar registo", "show-context": "Mostrar contexto", "show-details": "Ocultar detalhes do registo", @@ -11207,7 +11217,8 @@ "starting": "A iniciar...", "status": { "title-error-running-job": "Erro ao executar tarefa", - "title-job-completed-successfully": "Trabalho concluído com sucesso" + "title-job-completed-successfully": "Trabalho concluído com sucesso", + "title-warning-running-job": "" }, "summary": "Resumo" }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index a16f5fd8f37..d2f6a44a18d 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1563,7 +1564,8 @@ "title": "Редактирование группы оценки" }, "group-loader": { - "group-load-failed": "Не удалось загрузить правила из группы {{ groupName }} в {{ namespaceName }}" + "group-load-failed": "Не удалось загрузить правила из группы {{ groupName }} в {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Выполняется удаление группы" @@ -2828,6 +2830,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Обновить просмотр", + "empty-state": "", "label-preview": "Предварительный просмотр", "refresh": "Обновить" }, @@ -8641,7 +8644,10 @@ "no-data": "" }, "no-values-label": "Нет значений", - "pagination-summary": "Строки: {{itemsRangeStart}} - {{displayedEnd}} из {{numRows}}" + "pagination-summary": "Строки: {{itemsRangeStart}} - {{displayedEnd}} из {{numRows}}", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Теги" @@ -9390,6 +9396,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9408,6 +9415,9 @@ "copy-log": "Копировать строку журнала", "hide-details": "Показать сведения о журнале", "icon-label": "Меню журнала", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Закрепить журнал", "show-context": "Показать контекст", "show-details": "Скрыть сведения о журнале", @@ -11279,7 +11289,8 @@ "starting": "Запуск...", "status": { "title-error-running-job": "Ошибка при выполнении задания", - "title-job-completed-successfully": "Задание завершено" + "title-job-completed-successfully": "Задание завершено", + "title-warning-running-job": "" }, "summary": "Сводка" }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 8478e730cef..adbc94f9c1c 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Redigera utvärderingsgrupp" }, "group-loader": { - "group-load-failed": "Det gick inte att ladda reglerna från gruppen {{ groupName }} i {{ namespaceName }}" + "group-load-failed": "Det gick inte att ladda reglerna från gruppen {{ groupName }} i {{ namespaceName }}", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Gruppen raderas" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Uppdatera förhandsgranskning", + "empty-state": "", "label-preview": "Förhandsgranska", "refresh": "Uppdatera" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Inga värden", - "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} av {{numRows}} rader" + "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} av {{numRows}} rader", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Taggar" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Kopiera loggrad", "hide-details": "Visa loggdetaljer", "icon-label": "Loggmeny", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Fäst logg", "show-context": "Visa sammanhang", "show-details": "Dölj loggdetaljer", @@ -11207,7 +11217,8 @@ "starting": "Startar …", "status": { "title-error-running-job": "Ett fel uppstod när ett jobb kördes", - "title-job-completed-successfully": "Jobbet har slutförts" + "title-job-completed-successfully": "Jobbet har slutförts", + "title-warning-running-job": "" }, "summary": "Sammanfattning" }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index f8c079e5b78..aa90d1b2954 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1551,7 +1552,8 @@ "title": "Değerlendirme grubunu düzenle" }, "group-loader": { - "group-load-failed": "{{ namespaceName }} alanındaki {{ groupName }} grubundan kurallar yüklenemedi" + "group-load-failed": "{{ namespaceName }} alanındaki {{ groupName }} grubundan kurallar yüklenemedi", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "Grup siliniyor" @@ -2802,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Ön izlemeyi yenile", + "empty-state": "", "label-preview": "Ön izleme", "refresh": "Yenile" }, @@ -8597,7 +8600,10 @@ "no-data": "" }, "no-values-label": "Değer yok", - "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}}/{{numRows}} satır" + "pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}}/{{numRows}} satır", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "Etiketler" @@ -9330,6 +9336,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "", + "open-assistant": "", "pin-line": "", "remove-displayed-field": "", "remove-log": "", @@ -9348,6 +9355,9 @@ "copy-log": "Günlük satırını kopyala", "hide-details": "Günlük ayrıntılarını göster", "icon-label": "Günlük menüsü", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "Günlüğü sabitle", "show-context": "İçeriği göster", "show-details": "Günlük ayrıntılarını gizle", @@ -11207,7 +11217,8 @@ "starting": "Başlatılıyor...", "status": { "title-error-running-job": "İş çalıştırılırken hata oluştu", - "title-job-completed-successfully": "İş başarıyla tamamlandı" + "title-job-completed-successfully": "İş başarıyla tamamlandı", + "title-warning-running-job": "" }, "summary": "Özet" }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index d87c90a8735..003beb4e68f 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1545,7 +1546,8 @@ "title": "编辑评估组" }, "group-loader": { - "group-load-failed": "从 {{ namespaceName }} 的组 {{ groupName }} 中加载规则失败" + "group-load-failed": "从 {{ namespaceName }} 的组 {{ groupName }} 中加载规则失败", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "正在删除组" @@ -2789,6 +2791,7 @@ }, "template-preview": { "aria-label-refresh-preview": "刷新预览", + "empty-state": "", "label-preview": "预览", "refresh": "刷新" }, @@ -8575,7 +8578,10 @@ "no-data": "" }, "no-values-label": "没有值", - "pagination-summary": "第 {{itemsRangeStart}} - {{displayedEnd}} 行,共 {{numRows}} 行" + "pagination-summary": "第 {{itemsRangeStart}} - {{displayedEnd}} 行,共 {{numRows}} 行", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "标签" @@ -9300,6 +9306,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "没有要显示的字段。", + "open-assistant": "", "pin-line": "固定日志", "remove-displayed-field": "", "remove-log": "", @@ -9318,6 +9325,9 @@ "copy-log": "复制日志行", "hide-details": "显示日志详情", "icon-label": "日志菜单", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "固定日志", "show-context": "显示上下文", "show-details": "隐藏日志详情", @@ -11171,7 +11181,8 @@ "starting": "正在启动...", "status": { "title-error-running-job": "运行作业时出错", - "title-job-completed-successfully": "作业已成功完成" + "title-job-completed-successfully": "作业已成功完成", + "title-warning-running-job": "" }, "summary": "摘要" }, diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 3e0ec426112..375858bf88d 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "", + "feedback-button": "", "improve-annotations-button": "", "improve-labels-button": "", "template-button": "", @@ -1545,7 +1546,8 @@ "title": "編輯評估群組" }, "group-loader": { - "group-load-failed": "無法從「{{ namespaceName }}」中的群組「{{ groupName }}」載入規則" + "group-load-failed": "無法從「{{ namespaceName }}」中的群組「{{ groupName }}」載入規則", + "no-rules": "" }, "group-status": { "content-the-group-is-being-deleted": "正在刪除群組" @@ -2789,6 +2791,7 @@ }, "template-preview": { "aria-label-refresh-preview": "重新整理預覽", + "empty-state": "", "label-preview": "預覽", "refresh": "更新" }, @@ -8575,7 +8578,10 @@ "no-data": "" }, "no-values-label": "沒有數值", - "pagination-summary": "{{itemsRangeStart}} - 第 {{displayedEnd}} 列,共 {{numRows}} 列" + "pagination-summary": "{{itemsRangeStart}} - 第 {{displayedEnd}} 列,共 {{numRows}} 列", + "sparkline": { + "no-data": "" + } }, "tags": { "list-label": "標籤" @@ -9300,6 +9306,7 @@ "move-displayed-field-down": "", "move-displayed-field-up": "", "no-details": "沒有要顯示的欄位。", + "open-assistant": "", "pin-line": "固定紀錄", "remove-displayed-field": "", "remove-log": "", @@ -9318,6 +9325,9 @@ "copy-log": "複製日誌行", "hide-details": "顯示紀錄詳細資料", "icon-label": "日誌功能表", + "log-line": "", + "log-line-explainer": "", + "open-assistant": "", "pin-to-outline": "釘選日誌", "show-context": "顯示內容", "show-details": "隱藏紀錄詳細資料", @@ -11171,7 +11181,8 @@ "starting": "正在開始…", "status": { "title-error-running-job": "執行作業時發生錯誤", - "title-job-completed-successfully": "作業已成功完成" + "title-job-completed-successfully": "作業已成功完成", + "title-warning-running-job": "" }, "summary": "摘要" }, From d4a609419a050e2e7e344c7de6b0d5720bdc0e46 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 24 Jul 2025 08:15:17 +0300 Subject: [PATCH 041/101] Provisioning: Extend Git-based workflows (#108537) * Provisioning: Replace GitHub check with generic Git provider check * Provisioning: Replace GitHub check with generic Git provider check * Fix types * Codeformat * Imports --- .../BulkDeleteProvisionedResource.tsx | 15 +---- .../DeleteProvisionedFolderForm.test.tsx | 1 - .../DeleteProvisionedFolderForm.tsx | 16 +---- .../NewProvisionedFolderForm.test.tsx | 1 - .../components/NewProvisionedFolderForm.tsx | 8 +-- .../hooks/useProvisionedFolderFormData.ts | 3 - .../ResourceEditFormSharedFields.test.tsx | 60 +++++++++++++------ .../ResourceEditFormSharedFields.tsx | 8 ++- .../provisioned/SaveProvisionedDashboard.tsx | 4 +- .../SaveProvisionedDashboardForm.test.tsx | 8 ++- .../SaveProvisionedDashboardForm.tsx | 7 ++- .../saving/provisioned/hooks.ts | 6 +- .../DeleteProvisionedDashboardDrawer.tsx | 4 +- .../DeleteProvisionedDashboardForm.test.tsx | 1 - .../DeleteProvisionedDashboardForm.tsx | 8 +-- .../MoveProvisionedDashboardDrawer.tsx | 4 +- .../MoveProvisionedDashboardForm.test.tsx | 8 ++- .../settings/MoveProvisionedDashboardForm.tsx | 8 ++- 18 files changed, 90 insertions(+), 80 deletions(-) diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx index 65e3463d60e..77730c5320c 100644 --- a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx @@ -38,7 +38,6 @@ interface FormProps extends BulkDeleteProvisionResourceProps { initialValues: BulkDeleteFormData; repository: RepositoryView; workflowOptions: Array<{ label: string; value: string }>; - isGitHub: boolean; folderPath?: string; } @@ -59,15 +58,7 @@ type MoveResultSuccessState = { repoUrl?: string; }; -function FormContent({ - initialValues, - selectedItems, - repository, - workflowOptions, - folderPath, - isGitHub, - onDismiss, -}: FormProps) { +function FormContent({ initialValues, selectedItems, repository, workflowOptions, folderPath, onDismiss }: FormProps) { // States const [progress, setProgress] = useState(null); const [failureResults, setFailureResults] = useState(); @@ -233,7 +224,7 @@ function FormContent({ isNew={false} workflow={workflow} workflowOptions={workflowOptions} - isGitHub={isGitHub} + repository={repository} hidePath /> @@ -263,7 +254,6 @@ export function BulkDeleteProvisionedResource({ const { repository, folder } = useGetResourceRepositoryView({ folderName: folderUid }); const workflowOptions = getWorkflowOptions(repository); - const isGitHub = repository?.type === 'github'; const folderPath = folder?.metadata?.annotations?.[AnnoKeySourcePath] || ''; const timestamp = generateTimestamp(); @@ -284,7 +274,6 @@ export function BulkDeleteProvisionedResource({ initialValues={initialValues} repository={repository} workflowOptions={workflowOptions} - isGitHub={isGitHub} folderPath={folderPath} /> ); diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx index 454db4db534..7bd6c28587a 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx @@ -111,7 +111,6 @@ const defaultHookData: ProvisionedFolderFormDataResult = { { label: 'Write directly', value: 'write' }, { label: 'Create branch', value: 'branch' }, ], - isGitHub: true, repository: mockRepository, folder: mockFolder, initialValues: mockFormData, diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx index 2491b7bc506..ccf09a2f7c7 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx @@ -23,7 +23,6 @@ interface FormProps extends DeleteProvisionedFolderFormProps { repository?: RepositoryView; workflowOptions: Array<{ label: string; value: string }>; folder?: Folder; - isGitHub: boolean; } interface DeleteProvisionedFolderFormProps { @@ -31,15 +30,7 @@ interface DeleteProvisionedFolderFormProps { onDismiss?: () => void; } -function FormContent({ - initialValues, - parentFolder, - repository, - workflowOptions, - folder, - isGitHub, - onDismiss, -}: FormProps) { +function FormContent({ initialValues, parentFolder, repository, workflowOptions, folder, onDismiss }: FormProps) { const resourceId = parentFolder?.uid || ''; const [deleteRepoFile, request] = useDeleteRepositoryFilesWithPathMutation(); @@ -128,7 +119,7 @@ function FormContent({ isNew={false} workflow={workflow} workflowOptions={workflowOptions} - isGitHub={isGitHub} + repository={repository} /> {/* Delete / Cancel button */} @@ -149,7 +140,7 @@ function FormContent({ } export function DeleteProvisionedFolderForm({ parentFolder, onDismiss }: DeleteProvisionedFolderFormProps) { - const { workflowOptions, isGitHub, repository, folder, initialValues } = useProvisionedFolderFormData({ + const { workflowOptions, repository, folder, initialValues } = useProvisionedFolderFormData({ folderUid: parentFolder?.uid, action: 'delete', title: parentFolder?.title, @@ -167,7 +158,6 @@ export function DeleteProvisionedFolderForm({ parentFolder, onDismiss }: DeleteP repository={repository} workflowOptions={workflowOptions} folder={folder} - isGitHub={isGitHub} /> ); } diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx index c12c902228c..2242a8965ae 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx @@ -146,7 +146,6 @@ const mockHookData: ProvisionedFolderFormDataResult = { { label: 'Commit directly', value: 'write' }, { label: 'Create a branch', value: 'branch' }, ], - isGitHub: true, initialValues: { title: '', comment: '', diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx index f7f141fac01..9e35078c946 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx @@ -26,14 +26,13 @@ interface FormProps extends Props { repository?: RepositoryView; workflowOptions: Array<{ label: string; value: string }>; folder?: Folder; - isGitHub: boolean; } interface Props { parentFolder?: FolderDTO; onDismiss?: () => void; } -function FormContent({ initialValues, repository, workflowOptions, folder, isGitHub, onDismiss }: FormProps) { +function FormContent({ initialValues, repository, workflowOptions, folder, onDismiss }: FormProps) { const { prURL } = usePullRequestParam(); const navigate = useNavigate(); const [create, request] = useCreateRepositoryFilesWithPathMutation(); @@ -159,7 +158,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit isNew={false} workflow={workflow} workflowOptions={workflowOptions} - isGitHub={isGitHub} + repository={repository} hidePath /> @@ -197,7 +196,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, isGit } export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) { - const { workflowOptions, isGitHub, repository, folder, initialValues } = useProvisionedFolderFormData({ + const { workflowOptions, repository, folder, initialValues } = useProvisionedFolderFormData({ folderUid: parentFolder?.uid, action: 'create', title: '', // Empty title for new folders @@ -215,7 +214,6 @@ export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) { repository={repository} workflowOptions={workflowOptions} folder={folder} - isGitHub={isGitHub} /> ); } diff --git a/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts b/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts index 6fbc5f6c6eb..8ea8c4835f0 100644 --- a/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts +++ b/public/app/features/browse-dashboards/hooks/useProvisionedFolderFormData.ts @@ -19,7 +19,6 @@ export interface ProvisionedFolderFormDataResult { repository?: RepositoryView; folder?: Folder; workflowOptions: Array<{ label: string; value: string }>; - isGitHub: boolean; initialValues?: BaseProvisionedFormData; } @@ -34,7 +33,6 @@ export function useProvisionedFolderFormData({ const { repository, folder, isLoading } = useGetResourceRepositoryView({ folderName: folderUid }); const workflowOptions = getWorkflowOptions(repository); - const isGitHub = repository?.type === 'github'; const timestamp = generateTimestamp(); const initialValues = useMemo(() => { @@ -57,7 +55,6 @@ export function useProvisionedFolderFormData({ repository, folder, workflowOptions, - isGitHub, initialValues, }; } diff --git a/public/app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields.test.tsx b/public/app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields.test.tsx index 186262741bc..ae5a6d22b10 100644 --- a/public/app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields.test.tsx +++ b/public/app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields.test.tsx @@ -3,14 +3,32 @@ import userEvent from '@testing-library/user-event'; import { ReactNode } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { RepositoryView } from '../../../../api/clients/provisioning/v0alpha1'; import { ProvisionedDashboardFormData } from '../../saving/shared'; import { ResourceEditFormSharedFields } from './ResourceEditFormSharedFields'; +const mockRepo: { github: RepositoryView; local: RepositoryView } = { + github: { + type: 'github', + name: 'test-repo', + title: 'Test Repo', + workflows: ['branch', 'write'], + target: 'folder', + }, + local: { + type: 'local', + name: 'local-repo', + title: 'Local Repo', + workflows: [], + target: 'folder', + }, +}; + // Mock the i18n hook since it's used in the component jest.mock('@grafana/i18n', () => ({ t: (_: string, defaultValue: string) => defaultValue, - Trans: ({ children }: { children: React.ReactNode }) => children, + Trans: ({ children }: { children: ReactNode }) => children, })); interface SetupOptions { @@ -19,7 +37,7 @@ interface SetupOptions { isNew?: boolean; readOnly?: boolean; workflow?: 'write' | 'branch'; - isGitHub?: boolean; + repository?: RepositoryView; } function setup(options: SetupOptions = {}) { @@ -32,7 +50,7 @@ function setup(options: SetupOptions = {}) { isNew, readOnly, workflow, - isGitHub, + repository, } = options; const user = userEvent.setup(); @@ -58,7 +76,7 @@ function setup(options: SetupOptions = {}) { isNew, readOnly, workflow, - isGitHub, + repository, }; return { @@ -80,14 +98,14 @@ describe('ResourceEditFormSharedFields', () => { expect(screen.getByRole('textbox', { name: 'Comment' })).toBeInTheDocument(); }); - it('should not render workflow fields when isGitHub is false', () => { - setup({ isGitHub: false }); + it('should not render workflow fields when repository is false', () => { + setup({ repository: mockRepo.local }); expect(screen.queryByText('Workflow')).not.toBeInTheDocument(); }); - it('should render workflow fields when isGitHub is true', () => { - setup({ isGitHub: true }); + it('should render workflow fields when repository is true', () => { + setup({ repository: mockRepo.github }); expect(screen.getByRole('radiogroup')).toBeInTheDocument(); expect(screen.getByRole('radio', { name: 'Write directly' })).toBeInTheDocument(); @@ -110,8 +128,8 @@ describe('ResourceEditFormSharedFields', () => { expect(commentTextarea).toBeDisabled(); }); - it('should not render workflow fields when readOnly is true and isGitHub is true', () => { - setup({ readOnly: true, isGitHub: true }); + it('should not render workflow fields when readOnly is true and repository is true', () => { + setup({ readOnly: true, repository: mockRepo.github }); expect(screen.queryByText('Workflow')).not.toBeInTheDocument(); }); @@ -119,14 +137,14 @@ describe('ResourceEditFormSharedFields', () => { describe('Workflow Fields', () => { it('should not render branch field when workflow is write', () => { - setup({ formDefaultValues: { workflow: 'write' }, isGitHub: true, workflow: 'write' }); + setup({ formDefaultValues: { workflow: 'write' }, repository: mockRepo.github, workflow: 'write' }); expect(screen.getByText('Workflow')).toBeInTheDocument(); expect(screen.queryByRole('textbox', { name: /branch/i })).not.toBeInTheDocument(); }); it('should render branch field when workflow is branch', () => { - setup({ formDefaultValues: { workflow: 'branch' }, isGitHub: true, workflow: 'branch' }); + setup({ formDefaultValues: { workflow: 'branch' }, repository: mockRepo.github, workflow: 'branch' }); expect(screen.getByText('Workflow')).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: /branch/i })).toBeInTheDocument(); @@ -154,7 +172,7 @@ describe('ResourceEditFormSharedFields', () => { }); it('should allow selecting workflow options', async () => { - const { user } = setup({ isGitHub: true }); + const { user } = setup({ repository: mockRepo.github }); const branchOption = screen.getByRole('radio', { name: 'Create branch' }); await user.click(branchOption); @@ -163,7 +181,11 @@ describe('ResourceEditFormSharedFields', () => { }); it('should allow typing in branch field when workflow is branch', async () => { - const { user } = setup({ formDefaultValues: { workflow: 'branch' }, isGitHub: true, workflow: 'branch' }); + const { user } = setup({ + formDefaultValues: { workflow: 'branch' }, + repository: mockRepo.github, + workflow: 'branch', + }); const branchInput = screen.getByRole('textbox', { name: /branch/i }); await user.type(branchInput, 'feature-branch'); @@ -214,7 +236,11 @@ describe('ResourceEditFormSharedFields', () => { describe('Validation', () => { it('should show validation error for invalid branch name', async () => { - const { user } = setup({ formDefaultValues: { workflow: 'branch' }, isGitHub: true, workflow: 'branch' }); + const { user } = setup({ + formDefaultValues: { workflow: 'branch' }, + repository: mockRepo.github, + workflow: 'branch', + }); const branchInput = screen.getByRole('textbox', { name: /branch/i }); await user.type(branchInput, 'invalid//branch'); // Invalid branch name with consecutive slashes @@ -229,14 +255,14 @@ describe('ResourceEditFormSharedFields', () => { describe('Edge Cases', () => { it('should handle empty workflowOptions', () => { - setup({ workflowOptions: [], isGitHub: true }); + setup({ workflowOptions: [], repository: mockRepo.github }); expect(screen.getByText('Workflow')).toBeInTheDocument(); expect(screen.queryByRole('radio')).not.toBeInTheDocument(); }); it('should handle undefined props', () => { - setup({ readOnly: undefined, isGitHub: undefined }); + setup({ readOnly: undefined, repository: mockRepo.local }); expect(screen.getByRole('textbox', { name: /Path/ })).toBeInTheDocument(); expect(screen.getByRole('textbox', { name: 'Comment' })).toBeInTheDocument(); diff --git a/public/app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields.tsx b/public/app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields.tsx index 0582aca19e0..7940da956f0 100644 --- a/public/app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields.tsx +++ b/public/app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields.tsx @@ -3,9 +3,11 @@ import { Controller, useFormContext } from 'react-hook-form'; import { t } from '@grafana/i18n'; import { Field, TextArea, Input, RadioButtonGroup } from '@grafana/ui'; +import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; import { BranchValidationError } from 'app/features/provisioning/Shared/BranchValidationError'; import { WorkflowOption } from 'app/features/provisioning/types'; import { validateBranchName } from 'app/features/provisioning/utils/git'; +import { isGitProvider } from 'app/features/provisioning/utils/repositoryTypes'; interface DashboardEditFormSharedFieldsProps { resourceType: 'dashboard' | 'folder'; @@ -13,12 +15,12 @@ interface DashboardEditFormSharedFieldsProps { isNew?: boolean; readOnly?: boolean; workflow?: WorkflowOption; - isGitHub?: boolean; + repository?: RepositoryView; hidePath?: boolean; } export const ResourceEditFormSharedFields = memo( - ({ readOnly = false, workflow, workflowOptions, isGitHub, isNew, resourceType, hidePath = false }) => { + ({ readOnly = false, workflow, workflowOptions, repository, isNew, resourceType, hidePath = false }) => { const { control, register, @@ -64,7 +66,7 @@ export const ResourceEditFormSharedFields = memo {/* Workflow */} - {isGitHub && !readOnly && ( + {repository?.type && isGitProvider(repository.type) && !readOnly && ( <> diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx index a17322b1dfb..18bc3a96cb4 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.test.tsx @@ -121,7 +121,13 @@ function setup(props: Partial = {}) { diffs: {}, }, isNew: true, - isGitHub: true, + repository: { + type: 'github', + name: 'test-repo', + title: 'Test Repo', + workflows: ['branch', 'write'], + target: 'folder', + }, defaultValues: { ref: 'dashboard/2023-01-01-abcde', path: 'test-dashboard.json', diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx index 7ffb27f7e9d..e20e4c811b0 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx @@ -7,6 +7,7 @@ import { Trans, t } from '@grafana/i18n'; import { getAppEvents, locationService } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Alert, Button, Field, Input, Stack, TextArea } from '@grafana/ui'; +import { RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import kbn from 'app/core/utils/kbn'; import { Resource } from 'app/features/apiserver/types'; @@ -26,10 +27,10 @@ import { getProvisionedMeta } from './utils/getProvisionedMeta'; export interface Props extends SaveProvisionedDashboardProps { isNew: boolean; defaultValues: ProvisionedDashboardFormData; - isGitHub: boolean; loadedFromRef?: string; workflowOptions: Array<{ label: string; value: string }>; readOnly: boolean; + repository?: RepositoryView; } export function SaveProvisionedDashboardForm({ @@ -39,9 +40,9 @@ export function SaveProvisionedDashboardForm({ changeInfo, isNew, loadedFromRef, - isGitHub, workflowOptions, readOnly, + repository, }: Props) { const navigate = useNavigate(); const appEvents = getAppEvents(); @@ -221,7 +222,7 @@ export function SaveProvisionedDashboardForm({ readOnly={readOnly} workflow={workflow} workflowOptions={workflowOptions} - isGitHub={isGitHub} + repository={repository} isNew={isNew} /> diff --git a/public/app/features/dashboard-scene/saving/provisioned/hooks.ts b/public/app/features/dashboard-scene/saving/provisioned/hooks.ts index e82ebcee548..8edec0ec7a2 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/hooks.ts +++ b/public/app/features/dashboard-scene/saving/provisioned/hooks.ts @@ -59,7 +59,6 @@ export function useDefaultValues({ meta, defaultTitle, defaultDescription, loade workflow: getDefaultWorkflow(repository, loadedFromRef), }, isNew: !meta.k8s?.name, - isGitHub: repository?.type === 'github', repository, }; } @@ -73,7 +72,6 @@ export interface ProvisionedDashboardData { loadedFromRef?: string; workflowOptions: Array<{ label: string; value: string }>; isNew: boolean; - isGitHub: boolean; readOnly: boolean; } @@ -105,12 +103,11 @@ export function useProvisionedDashboardData(dashboard: DashboardScene): Provisio loadedFromRef, workflowOptions: [], isNew: false, - isGitHub: false, readOnly: true, }; } - const { values, isNew, isGitHub, repository } = defaultValuesResult; + const { values, isNew, repository } = defaultValuesResult; const workflowOptions = getWorkflowOptions(repository, loadedFromRef); const readOnly = !repository?.workflows?.length; @@ -122,7 +119,6 @@ export function useProvisionedDashboardData(dashboard: DashboardScene): Provisio loadedFromRef, workflowOptions, isNew, - isGitHub, readOnly, isLoading, setIsLoading, diff --git a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardDrawer.tsx b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardDrawer.tsx index cc4d267e8d0..43f02e399e4 100644 --- a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardDrawer.tsx @@ -13,7 +13,7 @@ export interface Props { * Drawer component for deleting a git provisioned dashboard. */ export function DeleteProvisionedDashboardDrawer({ dashboard, onDismiss }: Props) { - const { defaultValues, loadedFromRef, readOnly, isGitHub, workflowOptions, isNew } = + const { defaultValues, loadedFromRef, readOnly, workflowOptions, isNew, repository } = useProvisionedDashboardData(dashboard); if (!defaultValues) { @@ -26,7 +26,7 @@ export function DeleteProvisionedDashboardDrawer({ dashboard, onDismiss }: Props defaultValues={defaultValues} loadedFromRef={loadedFromRef} readOnly={readOnly} - isGitHub={isGitHub} + repository={repository} isNew={isNew} workflowOptions={workflowOptions} onDismiss={onDismiss} diff --git a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.test.tsx index 128213ff1c7..9b05a88bf46 100644 --- a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.test.tsx @@ -107,7 +107,6 @@ function setup(options: SetupOptions = {}) { }, loadedFromRef: 'main', readOnly: false, - isGitHub: true, workflowOptions: [ { label: 'Branch', value: 'branch' }, { label: 'Write', value: 'write' }, diff --git a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx index e589205ff1c..d7b62a5cf22 100644 --- a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx @@ -5,7 +5,7 @@ import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { getAppEvents } from '@grafana/runtime'; import { Alert, Button, Drawer, Stack } from '@grafana/ui'; -import { useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; +import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields'; @@ -17,10 +17,10 @@ export interface Props { dashboard: DashboardScene; defaultValues: ProvisionedDashboardFormData; readOnly: boolean; - isGitHub: boolean; isNew?: boolean; workflowOptions: Array<{ label: string; value: string }>; loadedFromRef?: string; + repository?: RepositoryView; onDismiss: () => void; } @@ -33,9 +33,9 @@ export function DeleteProvisionedDashboardForm({ defaultValues, loadedFromRef, readOnly, - isGitHub, isNew, workflowOptions, + repository, onDismiss, }: Props) { const methods = useForm({ defaultValues }); @@ -127,7 +127,7 @@ export function DeleteProvisionedDashboardForm({ readOnly={readOnly} workflow={workflow} workflowOptions={workflowOptions} - isGitHub={isGitHub} + repository={repository} /> {/* Save / Cancel button */} diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx index b1caef27bf5..63bc6566680 100644 --- a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardDrawer.tsx @@ -18,7 +18,7 @@ export function MoveProvisionedDashboardDrawer({ onDismiss, onSuccess, }: Props) { - const { defaultValues, loadedFromRef, readOnly, isGitHub, workflowOptions, isNew } = + const { defaultValues, loadedFromRef, readOnly, workflowOptions, isNew, repository } = useProvisionedDashboardData(dashboard); if (!defaultValues) { @@ -31,7 +31,7 @@ export function MoveProvisionedDashboardDrawer({ defaultValues={defaultValues} loadedFromRef={loadedFromRef} readOnly={readOnly} - isGitHub={isGitHub} + repository={repository} isNew={isNew} workflowOptions={workflowOptions} targetFolderUID={targetFolderUID} diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx index 1e6496d4229..2c52d7ddb9a 100644 --- a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx @@ -70,7 +70,13 @@ function setup(props: Partial = {}) { folder: { uid: '', title: '' }, }, readOnly: false, - isGitHub: true, + repository: { + type: 'github', + name: 'test-repo', + title: 'Test Repo', + workflows: ['branch', 'write'], + target: 'folder', + }, workflowOptions: [ { label: 'Write', value: 'write' }, { label: 'Branch', value: 'branch' }, diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx index 6a17c8df03b..94951c10f46 100644 --- a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx @@ -8,6 +8,7 @@ import { getAppEvents } from '@grafana/runtime'; import { Alert, Button, Drawer, Field, Input, Spinner, Stack } from '@grafana/ui'; import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; import { + RepositoryView, useCreateRepositoryFilesWithPathMutation, useDeleteRepositoryFilesWithPathMutation, useGetRepositoryFilesWithPathQuery, @@ -23,12 +24,12 @@ export interface Props { dashboard: DashboardScene; defaultValues: ProvisionedDashboardFormData; readOnly: boolean; - isGitHub: boolean; isNew?: boolean; workflowOptions: Array<{ label: string; value: string }>; loadedFromRef?: string; targetFolderUID?: string; targetFolderTitle?: string; + repository?: RepositoryView; onDismiss: () => void; onSuccess: (folderUID: string, folderTitle: string) => void; } @@ -38,16 +39,17 @@ export function MoveProvisionedDashboardForm({ defaultValues, loadedFromRef, readOnly, - isGitHub, isNew, workflowOptions, targetFolderUID, targetFolderTitle, + repository, onDismiss, onSuccess, }: Props) { const methods = useForm({ defaultValues }); const { editPanel: panelEditor } = dashboard.useState(); + const { handleSubmit, watch } = methods; const appEvents = getAppEvents(); @@ -213,7 +215,7 @@ export function MoveProvisionedDashboardForm({ readOnly={readOnly} workflow={workflow} workflowOptions={workflowOptions} - isGitHub={isGitHub} + repository={repository} /> From 4922bb4065e02e09f82841babd21f6a9e468101c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 08:09:39 +0100 Subject: [PATCH 042/101] Bump marocchino/sticky-pull-request-comment from 2.9.3 to 2.9.4 (#108287) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/add-to-whats-new.yml | 2 +- .github/workflows/detect-breaking-changes-levitate.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/add-to-whats-new.yml b/.github/workflows/add-to-whats-new.yml index 699c6b7a220..a6a8bb8ea2e 100644 --- a/.github/workflows/add-to-whats-new.yml +++ b/.github/workflows/add-to-whats-new.yml @@ -10,7 +10,7 @@ jobs: permissions: pull-requests: write steps: - - uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 # v2.9.3 + - uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 with: message: | Since you've added the `Add to what's new` label, consider drafting a [What's new note](https://admin.grafana.com/content-admin/#/collections/whats-new/new) for this feature. diff --git a/.github/workflows/detect-breaking-changes-levitate.yml b/.github/workflows/detect-breaking-changes-levitate.yml index 85a35c35d67..ea7c12b4428 100644 --- a/.github/workflows/detect-breaking-changes-levitate.yml +++ b/.github/workflows/detect-breaking-changes-levitate.yml @@ -271,7 +271,7 @@ jobs: # Comment on the PR - name: Comment on PR if: steps.levitate-run.outputs.exit_code == 1 - uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 with: header: levitate-breaking-change-comment number: ${{ github.event.pull_request.number }} @@ -288,7 +288,7 @@ jobs: # Remove comment from the PR (no more breaking changes) - name: Remove comment from PR if: steps.levitate-run.outputs.exit_code == 0 - uses: marocchino/sticky-pull-request-comment@d2ad0de260ae8b0235ce059e63f2949ba9e05943 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 with: header: levitate-breaking-change-comment number: ${{ github.event.pull_request.number }} From 69731658a226458f3bd09b5bf6f63fe788fdf09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Thu, 24 Jul 2025 09:57:23 +0200 Subject: [PATCH 043/101] Span Details: Redesign details header (#108511) * Span Details: Redesign span details header * Update translations * Add max width to operation name * Test update We don't show the spanId anymore, just the "share" label for each trace --- .../SpanDetail/ShareSpanButton.tsx | 37 +++++++++++ .../SpanDetail/index.test.tsx | 2 +- .../TraceTimelineViewer/SpanDetail/index.tsx | 66 +++---------------- public/locales/en-US/grafana.json | 1 + 4 files changed, 49 insertions(+), 57 deletions(-) create mode 100644 public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/ShareSpanButton.tsx diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/ShareSpanButton.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/ShareSpanButton.tsx new file mode 100644 index 00000000000..23b76bb4b97 --- /dev/null +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/ShareSpanButton.tsx @@ -0,0 +1,37 @@ +import { LinkModel } from '@grafana/data'; +import { Trans } from '@grafana/i18n'; +import { Button } from '@grafana/ui'; + +type Props = { + focusSpanLink: LinkModel; +}; + +export function ShareSpanButton(props: Props) { + const { focusSpanLink } = props; + return ( + + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} + { + // click handling logic copied from react router: + // https://github.com/remix-run/react-router/blob/997b4d67e506d39ac6571cb369d6d2d6b3dda557/packages/react-router-dom/index.tsx#L392-L394s + if ( + focusSpanLink.onClick && + e.button === 0 && // Ignore everything but left clicks + (!e.currentTarget.target || e.currentTarget.target === '_self') && // Let browser handle "target=_blank" etc. + !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) // Ignore clicks with modifier keys + ) { + e.preventDefault(); + focusSpanLink.onClick(e); + } + }} + > + + + + ); +} diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx index 330b043ac82..dd1a23c12e8 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx @@ -250,7 +250,7 @@ describe('', () => { it('renders deep link URL', () => { render(); - expect(screen.getByText('test-spanID')).toBeInTheDocument(); + expect(screen.getByTestId('share-span-button')).toBeInTheDocument(); }); it('renders the flame graph', async () => { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index b9735b03720..67a6043f397 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -14,7 +14,6 @@ import { css } from '@emotion/css'; import { SpanStatusCode } from '@opentelemetry/api'; -import cx from 'classnames'; import { useCallback, useMemo } from 'react'; import { @@ -34,7 +33,7 @@ import { Trans, t } from '@grafana/i18n'; import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; import { usePluginLinks } from '@grafana/runtime'; import { TimeZone } from '@grafana/schema'; -import { Icon, TextArea, useStyles2 } from '@grafana/ui'; +import { TextArea, useStyles2 } from '@grafana/ui'; import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; import { autoColor } from '../../Theme'; @@ -49,6 +48,7 @@ import AccordianLogs from './AccordianLogs'; import AccordianReferences from './AccordianReferences'; import AccordianText from './AccordianText'; import DetailState from './DetailState'; +import { ShareSpanButton } from './ShareSpanButton'; import { getSpanDetailLinkButtons } from './SpanDetailLinkButtons'; import SpanFlameGraph from './SpanFlameGraph'; @@ -101,43 +101,22 @@ const getStyles = (theme: GrafanaTheme2) => { }), listWrapper: css({ overflow: 'hidden', + flexGrow: 1, + display: 'flex', + justifyContent: 'flex-end', }), list: css({ - textAlign: 'right', + textAlign: 'left', }), operationName: css({ margin: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', - flexBasis: '50%', + maxWidth: '50%', flexGrow: 0, flexShrink: 0, }), - debugInfo: css({ - label: 'debugInfo', - display: 'block', - letterSpacing: '0.25px', - margin: '0.5em 0 -0.75em', - textAlign: 'right', - }), - debugLabel: css({ - label: 'debugLabel', - '&::before': { - color: autoColor(theme, '#bbb'), - content: 'attr(data-label)', - }, - }), - debugValue: css({ - label: 'debugValue', - backgroundColor: 'inherit', - border: 'none', - color: autoColor(theme, '#888'), - cursor: 'pointer', - '&:hover': { - color: autoColor(theme, '#333'), - }, - }), AccordianWarnings: css({ label: 'AccordianWarnings', background: autoColor(theme, '#fafafa'), @@ -164,9 +143,6 @@ const getStyles = (theme: GrafanaTheme2) => { wordBreak: 'break-all', whiteSpace: 'pre', }), - LinkIcon: css({ - fontSize: '1.5em', - }), linkList: css({ display: 'flex', flexWrap: 'wrap', @@ -355,12 +331,13 @@ export default function SpanDetail(props: SpanDetailProps) { return (
-

+

{operationName} -
+
+
{linksComponent}
@@ -455,29 +432,6 @@ export default function SpanDetail(props: SpanDetailProps) { traceName={traceName} /> )} - - {/* TODO: fix keyboard a11y */} - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} - { - // click handling logic copied from react router: - // https://github.com/remix-run/react-router/blob/997b4d67e506d39ac6571cb369d6d2d6b3dda557/packages/react-router-dom/index.tsx#L392-L394s - if ( - focusSpanLink.onClick && - e.button === 0 && // Ignore everything but left clicks - (!e.currentTarget.target || e.currentTarget.target === '_self') && // Let browser handle "target=_blank" etc. - !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) // Ignore clicks with modifier keys - ) { - e.preventDefault(); - focusSpanLink.onClick(e); - } - }} - > - - - {spanID} -
); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index ddd03eccbf7..8a70d10f52d 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Start Time:" } }, + "share-span": "Share", "warnings": "Warnings" }, "span-filters": { From b1de5340920c71244166c46b4a647db4a9f5ac0f Mon Sep 17 00:00:00 2001 From: Mihai Turdean <6640685+mihai-turdean@users.noreply.github.com> Date: Thu, 24 Jul 2025 02:52:52 -0600 Subject: [PATCH 044/101] Refactor iam app cue files (#108542) A breaking change was introduced by this grafana-app-sdk PR: https://github.com/grafana/grafana-app-sdk/pull/795 This PR refactors the definition of our CUE files (manifest and kinds) to match the new model in the latest version of grafana-app-sdk. --- apps/iam/Makefile | 3 +- apps/iam/kinds/manifest.cue | 32 +++++++------ apps/iam/kinds/resourcepermission.cue | 21 ++++----- apps/iam/kinds/role.cue | 67 ++++++++++++--------------- apps/iam/kinds/rolebinding.cue | 44 ++++++++---------- apps/iam/kinds/serviceaccount.cue | 23 ++++----- apps/iam/kinds/team.cue | 20 ++++---- apps/iam/kinds/teambinding.cue | 21 ++++----- apps/iam/kinds/user.cue | 29 ++++-------- 9 files changed, 115 insertions(+), 145 deletions(-) diff --git a/apps/iam/Makefile b/apps/iam/Makefile index 4826c9b871f..6839689c867 100644 --- a/apps/iam/Makefile +++ b/apps/iam/Makefile @@ -8,5 +8,4 @@ generate: install-app-sdk update-app-sdk ## Run Grafana App SDK code generation --grouping=group \ --defencoding=none \ --noschemasinmanifest \ - --postprocess \ - --useoldmanifestkinds \ No newline at end of file + --postprocess \ \ No newline at end of file diff --git a/apps/iam/kinds/manifest.cue b/apps/iam/kinds/manifest.cue index 032261e9030..60a6d1f8faf 100644 --- a/apps/iam/kinds/manifest.cue +++ b/apps/iam/kinds/manifest.cue @@ -1,18 +1,24 @@ package kinds manifest: { - appName: "iam" + appName: "iam" groupOverride: "iam.grafana.app" - kinds: [ - globalrole, - globalrolebinding, - corerole, - role, - rolebinding, - resourcepermission, - user, - team, - teambinding, - serviceaccount, - ] + versions: { + "v0alpha1": v0alpha1 + } } + +v0alpha1: { + kinds: [ + globalrolev0alpha1, + globalrolebindingv0alpha1, + corerolev0alpha1, + rolev0alpha1, + rolebindingv0alpha1, + resourcepermissionv0alpha1, + userv0alpha1, + teamv0alpha1, + teambindingv0alpha1, + serviceaccountv0alpha1, + ] +} \ No newline at end of file diff --git a/apps/iam/kinds/resourcepermission.cue b/apps/iam/kinds/resourcepermission.cue index c40067b3e3f..42fcddfe6fc 100644 --- a/apps/iam/kinds/resourcepermission.cue +++ b/apps/iam/kinds/resourcepermission.cue @@ -4,20 +4,17 @@ import ( "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" ) -resourcepermission: { +resourcepermissionKind: { kind: "ResourcePermission" pluralName: "ResourcePermissions" - current: "v0alpha1" + codegen: { + ts: { enabled: false } + go: { enabled: true } + } +} - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.ResourcePermission - } - } +resourcepermissionv0alpha1: resourcepermissionKind & { + schema: { + spec: v0alpha1.ResourcePermission } } diff --git a/apps/iam/kinds/role.cue b/apps/iam/kinds/role.cue index 8ba1955ce89..1911fe83616 100644 --- a/apps/iam/kinds/role.cue +++ b/apps/iam/kinds/role.cue @@ -4,56 +4,47 @@ import ( "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" ) -role: { +roleKind: { kind: "Role" pluralName: "Roles" - current: "v0alpha1" - - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.RoleSpec - } - } + codegen: { + ts: { enabled: false } + go: { enabled: true } } } -corerole: { +coreroleKind: { kind: "CoreRole" pluralName: "CoreRoles" - current: "v0alpha1" - - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.RoleSpec - } - } + codegen: { + ts: { enabled: false } + go: { enabled: true } } } -globalrole: { +globalroleKind: { kind: "GlobalRole" pluralName: "GlobalRoles" - current: "v0alpha1" + codegen: { + ts: { enabled: false } + go: { enabled: true } + } +} - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.RoleSpec - } - } +rolev0alpha1: roleKind & { + schema: { + spec: v0alpha1.RoleSpec + } +} + +corerolev0alpha1: coreroleKind & { + schema: { + spec: v0alpha1.RoleSpec + } +} + +globalrolev0alpha1: globalroleKind & { + schema: { + spec: v0alpha1.RoleSpec } } \ No newline at end of file diff --git a/apps/iam/kinds/rolebinding.cue b/apps/iam/kinds/rolebinding.cue index 2cc0b955272..119b958970c 100644 --- a/apps/iam/kinds/rolebinding.cue +++ b/apps/iam/kinds/rolebinding.cue @@ -4,38 +4,32 @@ import ( "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" ) -rolebinding: { +rolebindingKind: { kind: "RoleBinding" pluralName: "RoleBindings" - current: "v0alpha1" - - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.RoleBindingSpec - } - } + codegen: { + ts: { enabled: false } + go: { enabled: true } } } -globalrolebinding: { +globalrolebindingKind: { kind: "GlobalRoleBinding" pluralName: "GlobalRoleBindings" - current: "v0alpha1" + codegen: { + ts: { enabled: false } + go: { enabled: true } + } +} - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.GlobalRoleBindingSpec - } - } +rolebindingv0alpha1: rolebindingKind & { + schema: { + spec: v0alpha1.RoleBindingSpec + } +} + +globalrolebindingv0alpha1: globalrolebindingKind & { + schema: { + spec: v0alpha1.GlobalRoleBindingSpec } } diff --git a/apps/iam/kinds/serviceaccount.cue b/apps/iam/kinds/serviceaccount.cue index e84b4ab0a0f..72b5c10025b 100644 --- a/apps/iam/kinds/serviceaccount.cue +++ b/apps/iam/kinds/serviceaccount.cue @@ -4,20 +4,17 @@ import ( "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" ) -serviceaccount: { +serviceaccountKind: { kind: "ServiceAccount" pluralName: "ServiceAccounts" - current: "v0alpha1" - - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.ServiceAccountSpec - } - } + codegen: { + ts: { enabled: false } + go: { enabled: true } } } + +serviceaccountv0alpha1: serviceaccountKind & { + schema: { + spec: v0alpha1.ServiceAccountSpec + } +} \ No newline at end of file diff --git a/apps/iam/kinds/team.cue b/apps/iam/kinds/team.cue index 0a8e980f4c0..9acf9d60c01 100644 --- a/apps/iam/kinds/team.cue +++ b/apps/iam/kinds/team.cue @@ -4,20 +4,18 @@ import ( "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" ) -team: { +teamKind: { kind: "Team" pluralName: "Teams" current: "v0alpha1" + codegen: { + ts: { enabled: false } + go: { enabled: true } + } +} - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.TeamSpec - } - } +teamv0alpha1: teamKind & { + schema: { + spec: v0alpha1.TeamSpec } } diff --git a/apps/iam/kinds/teambinding.cue b/apps/iam/kinds/teambinding.cue index 522a27b1880..60870b58853 100644 --- a/apps/iam/kinds/teambinding.cue +++ b/apps/iam/kinds/teambinding.cue @@ -4,20 +4,17 @@ import ( "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" ) -teambinding: { +teambindingKind: { kind: "TeamBinding" pluralName: "TeamBindings" - current: "v0alpha1" + codegen: { + ts: { enabled: false } + go: { enabled: true } + } +} - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - schema: { - spec: v0alpha1.TeamBindingSpec - } - } +teambindingv0alpha1: teambindingKind & { + schema: { + spec: v0alpha1.TeamBindingSpec } } diff --git a/apps/iam/kinds/user.cue b/apps/iam/kinds/user.cue index 4fe087701bf..480391dfa2e 100644 --- a/apps/iam/kinds/user.cue +++ b/apps/iam/kinds/user.cue @@ -4,26 +4,17 @@ import ( "github.com/grafana/grafana/apps/iam/kinds/v0alpha1" ) -user: { +userKind: { kind: "User" pluralName: "Users" - current: "v0alpha1" - - versions: { - "v0alpha1": { - codegen: { - ts: { enabled: false } - go: { enabled: true } - } - validation: { - operations: [ - "CREATE", - "UPDATE", - ] - } - schema: { - spec: v0alpha1.UserSpec - } - } + codegen: { + ts: { enabled: false } + go: { enabled: true } + } +} + +userv0alpha1: userKind & { + schema: { + spec: v0alpha1.UserSpec } } From 15d9df93f9a0b5927ebf5517d1044e1a55f9d784 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 24 Jul 2025 10:54:02 +0200 Subject: [PATCH 045/101] Fix: Prevent Rollup from treeshaking NPM packages (#108567) fix(packages): prevent rollup from treeshaking libraries --- packages/grafana-alerting/rollup.config.ts | 2 ++ packages/grafana-data/rollup.config.ts | 2 ++ packages/grafana-e2e-selectors/rollup.config.ts | 1 + packages/grafana-flamegraph/rollup.config.ts | 1 + packages/grafana-i18n/rollup.config.ts | 1 + packages/grafana-prometheus/rollup.config.ts | 1 + packages/grafana-runtime/rollup.config.ts | 2 ++ packages/grafana-schema/rollup.config.ts | 2 ++ packages/grafana-ui/rollup.config.ts | 2 ++ 9 files changed, 14 insertions(+) diff --git a/packages/grafana-alerting/rollup.config.ts b/packages/grafana-alerting/rollup.config.ts index bd1384bbe21..1d9b0e1ae40 100644 --- a/packages/grafana-alerting/rollup.config.ts +++ b/packages/grafana-alerting/rollup.config.ts @@ -10,10 +10,12 @@ export default [ input: entryPoint, plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-alerting')], + treeshake: false, }, { input: 'src/unstable.ts', plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-alerting')], + treeshake: false, }, ]; diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts index 98f8341f6de..378ab225769 100644 --- a/packages/grafana-data/rollup.config.ts +++ b/packages/grafana-data/rollup.config.ts @@ -10,10 +10,12 @@ export default [ input: entryPoint, plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')], + treeshake: false, }, { input: 'src/unstable.ts', plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-data')], + treeshake: false, }, ]; diff --git a/packages/grafana-e2e-selectors/rollup.config.ts b/packages/grafana-e2e-selectors/rollup.config.ts index 9888b316a6a..1d47540a42a 100644 --- a/packages/grafana-e2e-selectors/rollup.config.ts +++ b/packages/grafana-e2e-selectors/rollup.config.ts @@ -10,5 +10,6 @@ export default [ input: entryPoint, plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-e2e-selectors')], + treeshake: false, }, ]; diff --git a/packages/grafana-flamegraph/rollup.config.ts b/packages/grafana-flamegraph/rollup.config.ts index 3bc2d6a540f..967916710d6 100644 --- a/packages/grafana-flamegraph/rollup.config.ts +++ b/packages/grafana-flamegraph/rollup.config.ts @@ -10,5 +10,6 @@ export default [ input: entryPoint, plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-flamegraph')], + treeshake: false, }, ]; diff --git a/packages/grafana-i18n/rollup.config.ts b/packages/grafana-i18n/rollup.config.ts index 3171639a367..1b1115f65c9 100644 --- a/packages/grafana-i18n/rollup.config.ts +++ b/packages/grafana-i18n/rollup.config.ts @@ -16,5 +16,6 @@ export default [ }), ], output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-i18n')], + treeshake: false, }, ]; diff --git a/packages/grafana-prometheus/rollup.config.ts b/packages/grafana-prometheus/rollup.config.ts index be421e6c7aa..8dbc60c8935 100644 --- a/packages/grafana-prometheus/rollup.config.ts +++ b/packages/grafana-prometheus/rollup.config.ts @@ -13,5 +13,6 @@ export default [ input: entryPoint, plugins: [...plugins, image(), json(), dynamicImportVars()], output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-prometheus')], + treeshake: false, }, ]; diff --git a/packages/grafana-runtime/rollup.config.ts b/packages/grafana-runtime/rollup.config.ts index 7255b34b0d5..5807c919136 100644 --- a/packages/grafana-runtime/rollup.config.ts +++ b/packages/grafana-runtime/rollup.config.ts @@ -10,10 +10,12 @@ export default [ input: entryPoint, plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-runtime')], + treeshake: false, }, { input: 'src/unstable.ts', plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-runtime')], + treeshake: false, }, ]; diff --git a/packages/grafana-schema/rollup.config.ts b/packages/grafana-schema/rollup.config.ts index e274b31df26..04928f75c47 100644 --- a/packages/grafana-schema/rollup.config.ts +++ b/packages/grafana-schema/rollup.config.ts @@ -16,6 +16,7 @@ export default [ input: entryPoint, plugins, output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-schema')], + treeshake: false, }, { input: Object.fromEntries( @@ -45,5 +46,6 @@ export default [ format: 'esm', dir: path.dirname(pkg.publishConfig.module), }, + treeshake: false, }, ]; diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts index bf652c8d21f..16c81ca4af8 100644 --- a/packages/grafana-ui/rollup.config.ts +++ b/packages/grafana-ui/rollup.config.ts @@ -25,6 +25,7 @@ export default [ }), ], output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')], + treeshake: false, }, { input: 'src/unstable.ts', @@ -37,5 +38,6 @@ export default [ }), ], output: [cjsOutput(pkg), esmOutput(pkg, 'grafana-ui')], + treeshake: false, }, ]; From 559d3a274a952eeea1bdf818b4f4cf8de218c187 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 24 Jul 2025 10:41:56 +0100 Subject: [PATCH 046/101] Chore: Bump semver version for pa11y (#108573) --- package.json | 3 +-- yarn.lock | 11 ----------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/package.json b/package.json index 0078e8b89e4..299f992444a 100644 --- a/package.json +++ b/package.json @@ -433,8 +433,7 @@ "resolutions": { "underscore": "1.13.7", "@types/slate": "0.47.11", - "semver@~7.0.0": "7.5.4", - "semver@7.3.4": "7.5.4", + "semver@npm:~7.3.5": "^7.3.5", "debug@npm:^0.7.2": "2.6.9", "debug@npm:^0.7.4": "2.6.9", "slate-dev-environment@^0.2.2": "patch:slate-dev-environment@npm:0.2.5#.yarn/patches/slate-dev-environment-npm-0.2.5-9aeb7da7b5.patch", diff --git a/yarn.lock b/yarn.lock index dc03bc3f24b..cf063b2ba73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28093,17 +28093,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:~7.3.5": - version: 7.3.8 - resolution: "semver@npm:7.3.8" - dependencies: - lru-cache: "npm:^6.0.0" - bin: - semver: bin/semver.js - checksum: 10/c8c04a4d41d30cffa7277904e0ad6998623dd61e36bca9578b0128d8c683b705a3924beada55eae7fa004fb30a9359a53a4ead2b68468d778b602f3b1a28f8e3 - languageName: node - linkType: hard - "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" From d852bde2a5fb078828157e4e86e9ea64de46d4ce Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 24 Jul 2025 11:53:30 +0200 Subject: [PATCH 047/101] Fix: plugin loader cache not matching on decoupled plugin paths (#108529) * fix(plugins): match decoupled plugin path in fe plugin loader cache * chore(devenv): update local_cdn to work with latest changes in core * refactor(plugins): improve legibility by splitting regexs into two * revert(devenv): revert -oss removal in local_cdn rewrite path --- devenv/local_cdn/default.conf | 2 +- public/app/features/plugins/loader/cache.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/devenv/local_cdn/default.conf b/devenv/local_cdn/default.conf index e35c16ee146..0c37c5ff410 100644 --- a/devenv/local_cdn/default.conf +++ b/devenv/local_cdn/default.conf @@ -19,6 +19,6 @@ server { add_header 'Access-Control-Allow-Headers' '*' always; add_header 'Access-Control-Allow-Credentials' 'true' always; - rewrite ^/grafana-oss/11.4.0-pre/public(.*)$ $1 last; + rewrite ^/grafana-oss/12.1.0-pre/public(.*)$ $1 last; } } diff --git a/public/app/features/plugins/loader/cache.ts b/public/app/features/plugins/loader/cache.ts index 00e77489efd..877d7fc485b 100644 --- a/public/app/features/plugins/loader/cache.ts +++ b/public/app/features/plugins/loader/cache.ts @@ -54,7 +54,20 @@ export function getPluginFromCache(path: string): CachedPlugin | undefined { export function extractCacheKeyFromPath(path: string) { const regex = /\/?public\/plugins\/([^\/]+)\//; const match = path.match(regex); - return match ? match[1] : null; + + if (match) { + return match[1]; + } + + // Decoupled core plugins can be loaded by alternative paths + const decoupledPluginRegex = /\/?public\/app\/plugins\/(?:datasource|panel)\/([^\/]+)\//; + const decoupledPluginMatch = path.match(decoupledPluginRegex); + + if (decoupledPluginMatch) { + return decoupledPluginMatch[1]; + } + + return null; } function getCacheKey(address: string): string | undefined { From 656c0b56b893fca9dab5b513e232f307790fe3d3 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 24 Jul 2025 11:29:12 +0100 Subject: [PATCH 048/101] Chore: Bump form-data (#108579) --- yarn.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cf063b2ba73..44c37605c3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17050,14 +17050,15 @@ __metadata: linkType: hard "form-data@npm:^4.0.0, form-data@npm:~4.0.0": - version: 4.0.2 - resolution: "form-data@npm:4.0.2" + version: 4.0.4 + resolution: "form-data@npm:4.0.4" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" mime-types: "npm:^2.1.12" - checksum: 10/82c65b426af4a40090e517a1bc9057f76970b4c6043e37aa49859c447d88553e77d4cc5626395079a53d2b0889ba5f2a49f3900db3ad3f3f1bf76613532572fb + checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb languageName: node linkType: hard From 5066aec64dfbc6381a494120239135956ca05304 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 24 Jul 2025 12:32:06 +0200 Subject: [PATCH 049/101] Advisor: Context handler returns a copy of request (#108508) --- apps/advisor/pkg/app/utils.go | 6 +- apps/advisor/pkg/app/utils_test.go | 114 +++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go index 2116102a4d9..80f71cc679e 100644 --- a/apps/advisor/pkg/app/utils.go +++ b/apps/advisor/pkg/app/utils.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana-app-sdk/resource" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/services/contexthandler" ) func getCheck(obj resource.Object, checkMap map[string]checks.Check) (checks.Check, error) { @@ -198,7 +199,10 @@ func runStepsInParallel(ctx context.Context, log logging.Logger, spec *advisorv0 } }() logger := log.With("step", step.ID()) - stepErr, err = step.Run(ctx, logger, spec, item) + // Create a copy of the context with a cloned HTTP request to prevent + // concurrent modifications to the same header map + safeCtx := contexthandler.CopyWithReqContext(ctx) + stepErr, err = step.Run(safeCtx, logger, spec, item) }() mu.Lock() defer mu.Unlock() diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go index 2ac90c2404e..663d7cfdcf6 100644 --- a/apps/advisor/pkg/app/utils_test.go +++ b/apps/advisor/pkg/app/utils_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/http" "testing" "github.com/grafana/grafana-app-sdk/logging" @@ -11,7 +12,13 @@ import ( advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetCheck(t *testing.T) { @@ -289,6 +296,67 @@ func TestProcessCheckRetry_Success(t *testing.T) { assert.Empty(t, obj.Status.Report.Failures) } +func TestRunStepsInParallel_ConcurrentHeaderAccess(t *testing.T) { + // Create an HTTP request with headers to simulate the real scenario + req, err := http.NewRequest("GET", "/test", nil) + require.NoError(t, err) + req.Header.Set("X-Test-Header", "test-value") + req.Header.Set("X-Panel-Id", "123") + req.Header.Set("Cookie", "session=abc123; user_pref=dark") + + // Create a context with ReqContext that includes the HTTP request + webCtx := &web.Context{ + Req: req, + } + reqCtx := &contextmodel.ReqContext{ + Context: webCtx, + SignedInUser: &user.SignedInUser{}, + } + + ctx := ctxkey.Set(context.Background(), reqCtx) + + // Create steps that modify headers concurrently (simulating CookiesMiddleware behavior) + steps := []checks.Step{ + &headerModifyingStep{headerName: "X-Test-Header", headerValue: "modified-1"}, + &headerModifyingStep{headerName: "Cookie", headerValue: "session=xyz456"}, + &headerModifyingStep{headerName: "X-Panel-Id", headerValue: "456"}, + } + + // Create multiple items to process + const numItems = 20 + items := make([]any, numItems) + for i := 0; i < numItems; i++ { + items[i] = fmt.Sprintf("item-%d", i) + } + + // Track panics that might occur during execution + var panicCount int32 + originalPanicHandler := func() { + if r := recover(); r != nil { + panicCount++ + t.Errorf("Unexpected panic during concurrent header access: %v", r) + } + } + + // This test should not panic with our fix + t.Run("should not panic with concurrent header modifications", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + originalPanicHandler() + } + }() + + failures, err := runStepsInParallel(ctx, logging.DefaultLogger, nil, steps, items) + + // Verify no error occurred + assert.NoError(t, err) + // Should have no failures since our mock step doesn't report failures + assert.Empty(t, failures) + // Verify no panics occurred + assert.Equal(t, int32(0), panicCount) + }) +} + type mockClient struct { resource.Client values []any @@ -377,3 +445,49 @@ func (m *mockStep) Resolution() string { func (m *mockStep) ID() string { return "mock" } + +// headerModifyingStep is a mock step that modifies HTTP headers to simulate +// the behavior of CookiesMiddleware and other middleware that caused the original panic +type headerModifyingStep struct { + headerName string + headerValue string +} + +func (h *headerModifyingStep) Run(ctx context.Context, log logging.Logger, obj *advisorv0alpha1.CheckSpec, item any) ([]advisorv0alpha1.CheckReportFailure, error) { + // Get the request context and modify headers (this used to cause panics) + reqCtx := contexthandler.FromContext(ctx) + if reqCtx != nil && reqCtx.Req != nil { + // This is the type of header modification that was causing the concurrent map access panic + reqCtx.Req.Header.Set(h.headerName, h.headerValue) + reqCtx.Req.Header.Add("X-Processed-By", h.ID()) + + // Also test header deletion like ClearCookieHeader does + if h.headerName == "Cookie" { + reqCtx.Req.Header.Del("Cookie") + reqCtx.Req.Header.Set("Cookie", h.headerValue) + } + + // Test reading headers as well + _ = reqCtx.Req.Header.Get("X-Test-Header") + _ = reqCtx.Req.Header.Get("X-Panel-Id") + } + + // No failures to report + return nil, nil +} + +func (h *headerModifyingStep) Title() string { + return "Header Modifying Step" +} + +func (h *headerModifyingStep) Description() string { + return "A mock step that modifies HTTP headers to test concurrent access" +} + +func (h *headerModifyingStep) Resolution() string { + return "This is a test step" +} + +func (h *headerModifyingStep) ID() string { + return fmt.Sprintf("header-modifier-%s", h.headerName) +} From 2e2bb605ea44ad6b36b8d0551fc4ae19aa6bd043 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:56:51 +0200 Subject: [PATCH 050/101] Alerting: Fix wrong import (#108581) * fix wrong import * add eslint rule to no-restricted-imports * eslint config update * eslint config update --- eslint.config.js | 18 ++++++++++++++++++ .../AI/addAIFeedbackButton.tsx | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index fd4802f10d4..c31f2f77930 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -410,4 +410,22 @@ module.exports = [ ], }, }, + { + name: 'grafana/no-extensions-imports', + files: ['**/*.{ts,tsx,js}'], + ignores: ['public/app/extensions/**/*'], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['app/extensions', 'app/extensions/*'], + message: 'Importing from app/extensions is not allowed', + }, + ], + }, + ], + }, + }, ]; diff --git a/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx b/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx index fae71645554..d0d68bcd0e8 100644 --- a/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx +++ b/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx @@ -2,10 +2,11 @@ import { ComponentType, createElement } from 'react'; import { t } from '@grafana/i18n'; import { withErrorBoundary } from '@grafana/ui'; -import { AIFeedbackOrigin } from 'app/extensions/alerting/AI/feedback/AIFeedbackComponent'; import { logError } from '../../Analytics'; +export type AIFeedbackOrigin = 'alert-rule' | 'template' | 'triage'; + export interface GenAIFeedbackButtonProps { origin: AIFeedbackOrigin; shouldShowFeedbackButton?: boolean; From 1c453cd81c58a3bbfd8f7d785d7a4c27d9707328 Mon Sep 17 00:00:00 2001 From: Victor Marin <36818606+mdvictor@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:03:16 +0300 Subject: [PATCH 051/101] Tests: Add user profile timezone settings in DashboardSceneSerializer test (#108584) fix tests --- .../serialization/DashboardSceneSerializer.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 0e39748c270..86dfce4fc4e 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -40,6 +40,9 @@ jest.mock('@grafana/runtime', () => ({ config: { ...jest.requireActual('@grafana/runtime').config, bootData: { + user: { + timezone: 'UTC', + }, settings: { defaultDatasource: '-- Grafana --', datasources: { From 4c17c1cbb6b8c14b4097a84922460b7e29ce01a4 Mon Sep 17 00:00:00 2001 From: Victor Cinaglia Date: Thu, 24 Jul 2025 09:33:53 -0300 Subject: [PATCH 052/101] IAM: Instantiate DualWriter only when in single-tenant mode (#108558) * Instantiate dual writer only in ST case * minor cleanup --------- Co-authored-by: Charandas Batra --- pkg/registry/apis/iam/models.go | 3 +++ pkg/registry/apis/iam/register.go | 31 ++++++++++++++++--------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index 64c4b40655c..63c74d6ed0e 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -43,4 +43,7 @@ type IdentityAccessManagementAPIBuilder struct { // Toggle for enabling authz management apis enableAuthZApis bool + + // Toggle for enabling dual writer + enableDualWriter bool } diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 936c11c23d9..eac9f6340c3 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -65,6 +65,7 @@ func RegisterAPIService( display: user.NewLegacyDisplayREST(store), reg: reg, enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis), + enableDualWriter: true, } apiregistration.RegisterAPI(builder) @@ -127,23 +128,23 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge userResource := legacyiamv0.UserResourceInfo legacyStore := user.NewLegacyStore(b.store, b.legacyAccessClient) - - // TODO: Figure out what's missing for the DualWriter setup in a MT setup - // MT app is unable to start if DW is configured - // store, err := grafanaregistry.NewRegistryStore(opts.Scheme, userResource, opts.OptsGetter) - // if err != nil { - // return err - // } - - // dw, err := opts.DualWriteBuilder(userResource.GroupResource(), legacyStore, store) - // if err != nil { - // return err - // } - - // storage[userResource.StoragePath()] = dw storage[userResource.StoragePath()] = legacyStore - storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store) + if b.enableDualWriter { + store, err := grafanaregistry.NewRegistryStore(opts.Scheme, userResource, opts.OptsGetter) + if err != nil { + return err + } + + dw, err := opts.DualWriteBuilder(userResource.GroupResource(), legacyStore, store) + if err != nil { + return err + } + + storage[userResource.StoragePath()] = dw + } + + storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store) serviceAccountResource := legacyiamv0.ServiceAccountResourceInfo storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store, b.legacyAccessClient) storage[serviceAccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store) From 1e8b065a0a68ac90be1afd11957586f5688845e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:56:07 +0000 Subject: [PATCH 053/101] Update scenes to v6.28.0 (#108548) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 44c37605c3d..03c67130507 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3610,10 +3610,10 @@ __metadata: linkType: soft "@grafana/scenes-react@npm:^6.27.2": - version: 6.27.2 - resolution: "@grafana/scenes-react@npm:6.27.2" + version: 6.28.1 + resolution: "@grafana/scenes-react@npm:6.28.1" dependencies: - "@grafana/scenes": "npm:6.27.2" + "@grafana/scenes": "npm:6.28.1" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3625,13 +3625,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/415186350304d999a13fa1e66f8bb7675cb28d5174a9449b8cb3d6a25efaecfc74a0e2e69cde9513f6fd9cac9bd2891c12e93a974ec15cfa0d6d04e5b95b5306 + checksum: 10/03278682a8ff7ebb399d522b3f54aee820cb34ab85f79d321ee44c4fbb1a490b6d62a8f03cae68187a70cc4345715d41441a72ae3915e6bebfe20fbfb61f9108 languageName: node linkType: hard -"@grafana/scenes@npm:6.27.2, @grafana/scenes@npm:^6.27.2": - version: 6.27.2 - resolution: "@grafana/scenes@npm:6.27.2" +"@grafana/scenes@npm:6.28.1, @grafana/scenes@npm:^6.27.2": + version: 6.28.1 + resolution: "@grafana/scenes@npm:6.28.1" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3651,7 +3651,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/8ba3d6c9b29de32a943a4a53511822f27eb51b35270368f0b9d80208eab50ae8767c6b372a7c7779a213618eab24bbb3e441dde42b038fc0a2078989879e0d14 + checksum: 10/603cb2b421e59a51ee36af6f1d6554e6f328db3aea42da7960e33c14de4ce298b2c261affa6dd651fe8d4ffb4a58e7825958a3b66d00daa3410e91f5f1fb185f languageName: node linkType: hard From b129b28d7e9d33369941a4d3b40cdde2ec5bafb6 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:05:56 +0200 Subject: [PATCH 054/101] Alerting: Fix action buttons flickering in the old list view (#108321) * fix action buttons flickering in the old list view * prettier * address review coments * remove duplication * add canCreateGrafanaRules check for ai button --- .../unified/rule-list/RuleList.v1.tsx | 66 ++--------------- .../components/RuleListActionButtons.tsx | 74 +++++++++++++++++++ 2 files changed, 79 insertions(+), 61 deletions(-) create mode 100644 public/app/features/alerting/unified/rule-list/components/RuleListActionButtons.tsx diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx index c716c6cbb37..bec19c1cc99 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx @@ -1,16 +1,13 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useLocation } from 'react-router-dom-v5-compat'; import { useAsyncFn, useInterval } from 'react-use'; -import { urlUtil } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; -import { logInfo } from '@grafana/runtime'; -import { Button, LinkButton, Stack } from '@grafana/ui'; +import { t } from '@grafana/i18n'; +import { Button, Stack } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useDispatch } from 'app/types/store'; import { CombinedRuleNamespace } from 'app/types/unified-alerting'; -import { LogMessages, trackRuleListNavigation } from '../Analytics'; +import { trackRuleListNavigation } from '../Analytics'; import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; import RulesFilter from '../components/rules/Filter/RulesFilter.v1'; import { NoRulesSplash } from '../components/rules/NoRulesCTA'; @@ -19,18 +16,16 @@ import { RuleListErrors } from '../components/rules/RuleListErrors'; import { RuleListGroupView } from '../components/rules/RuleListGroupView'; import { RuleListStateView } from '../components/rules/RuleListStateView'; import { RuleStats } from '../components/rules/RuleStats'; -import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton'; import { shouldUsePrometheusRulesPrimary } from '../featureToggles'; -import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities'; import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces'; import { useFilteredRules, useRulesFilter } from '../hooks/useFilteredRules'; import { useUnifiedAlertingSelector } from '../hooks/useUnifiedAlertingSelector'; import { fetchAllPromAndRulerRulesAction, fetchAllPromRulesAction, fetchRulerRulesAction } from '../state/actions'; import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames } from '../utils/datasource'; -import { createRelativeUrl } from '../utils/url'; import { RuleListPageTitle } from './RuleListPageTitle'; +import { RuleListActionButtons } from './components/RuleListActionButtons'; const VIEWS = { groups: RuleListGroupView, @@ -127,13 +122,7 @@ const RuleListV1 = () => { navId="alert-list" isLoading={false} renderTitle={(title) => } - actions={ - hasAlertRulesCreated && ( - - - - ) - } + actions={} > @@ -162,48 +151,3 @@ const RuleListV1 = () => { }; export default RuleListV1; - -export function CreateAlertButton() { - const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule); - const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule); - - const location = useLocation(); - - const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed; - - const canCreateGrafanaRules = createRuleSupported && createRuleAllowed; - - if (canCreateGrafanaRules || canCreateCloudRules) { - return ( - - logInfo(LogMessages.alertRuleFromScratch)} - > - New alert rule - - {canCreateGrafanaRules && AIAlertRuleButtonComponent && } - - ); - } - return null; -} - -function ExportNewRuleButton() { - const returnTo = window.location.pathname + window.location.search; - const url = createRelativeUrl(`/alerting/export-new-rule`, { - returnTo, - }); - return ( - logInfo(LogMessages.exportNewGrafanaRule)} - > - Export rule definition - - ); -} diff --git a/public/app/features/alerting/unified/rule-list/components/RuleListActionButtons.tsx b/public/app/features/alerting/unified/rule-list/components/RuleListActionButtons.tsx new file mode 100644 index 00000000000..97da3d7430e --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/components/RuleListActionButtons.tsx @@ -0,0 +1,74 @@ +import { memo } from 'react'; + +import { Trans, t } from '@grafana/i18n'; +import { logInfo } from '@grafana/runtime'; +import { LinkButton, Stack } from '@grafana/ui'; + +import { LogMessages } from '../../Analytics'; +import { AIAlertRuleButtonComponent } from '../../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton'; +import { AlertingAction, useAlertingAbility } from '../../hooks/useAbilities'; +import { createReturnTo } from '../../hooks/useReturnTo'; +import { createRelativeUrl } from '../../utils/url'; + +interface RuleListActionButtonsProps { + hasAlertRulesCreated: boolean; +} + +export const RuleListActionButtons = memo(({ hasAlertRulesCreated }) => { + if (!hasAlertRulesCreated) { + return null; + } + + return ( + + + + + ); +}); + +RuleListActionButtons.displayName = 'RuleListActionButtons'; + +function CreateAlertButtons() { + const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule); + const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule); + + const returnTo = createReturnTo(); + + const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed; + const canCreateGrafanaRules = createRuleSupported && createRuleAllowed; + + if (canCreateGrafanaRules || canCreateCloudRules) { + return ( + + {canCreateGrafanaRules && } + logInfo(LogMessages.alertRuleFromScratch)} + > + New alert rule + + + ); + } + return null; +} + +function ExportNewRuleButton() { + const returnTo = createReturnTo(); + const url = createRelativeUrl(`/alerting/export-new-rule`, { + returnTo, + }); + return ( + logInfo(LogMessages.exportNewGrafanaRule)} + > + Export rule definition + + ); +} From 0895d63516875b800f4d5216e39e2b3891fb1ff3 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 24 Jul 2025 09:22:44 -0400 Subject: [PATCH 055/101] Alerting: Remote Alertmanager to compare current hash with hash from response (#108554) --- pkg/services/ngalert/remote/alertmanager.go | 18 ++++++------------ .../ngalert/remote/alertmanager_test.go | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 83ffef2251f..e1d1621f770 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -300,18 +300,18 @@ func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config return fmt.Errorf("unable to marshal decrypted configuration: %w", err) } - configHash := md5.Sum(rawPayload) + configHash := fmt.Sprintf("%x", md5.Sum(rawPayload)) // Send the configuration only if we need to. if !am.shouldSendConfig(ctx, configHash) { return nil } - return am.sendConfiguration(ctx, payload, fmt.Sprintf("%x", configHash), config.CreatedAt, am.isDefaultConfiguration(configHash)) + return am.sendConfiguration(ctx, payload, configHash, config.CreatedAt, am.isDefaultConfiguration(configHash)) } -func (am *Alertmanager) isDefaultConfiguration(configHash [16]byte) bool { - return fmt.Sprintf("%x", configHash) == am.defaultConfigHash +func (am *Alertmanager) isDefaultConfiguration(configHash string) bool { + return configHash == am.defaultConfigHash } // decryptConfiguration creates a copy of the configuration, decrypts it, and returns the decrypted configuration alongside its hash. @@ -749,7 +749,7 @@ func (am *Alertmanager) getFullState(ctx context.Context) (string, error) { // shouldSendConfig compares the remote Alertmanager configuration with our local one. // It returns true if the configurations are different. -func (am *Alertmanager) shouldSendConfig(ctx context.Context, hash [16]byte) bool { +func (am *Alertmanager) shouldSendConfig(ctx context.Context, hash string) bool { rc, err := am.mimirClient.GetGrafanaAlertmanagerConfig(ctx) if err != nil { // Log the error and return true so we try to upload our config anyway. @@ -788,11 +788,5 @@ func (am *Alertmanager) shouldSendConfig(ctx context.Context, hash [16]byte) boo } } - // Hash and compare Alertmanager configs. - rawRemote, err := json.Marshal(rc.GrafanaAlertmanagerConfig) - if err != nil { - am.log.Error("Unable to marshal the remote Alertmanager configuration for comparison", "err", err) - return true - } - return md5.Sum(rawRemote) != hash + return rc.Hash != hash } diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 8e0b639734c..c8d379089cd 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -727,7 +727,7 @@ func Test_isDefaultConfiguration(t *testing.T) { } raw, err := json.Marshal(test.config) require.NoError(tt, err) - require.Equal(tt, test.expected, am.isDefaultConfiguration(md5.Sum(raw))) + require.Equal(tt, test.expected, am.isDefaultConfiguration(fmt.Sprintf("%x", md5.Sum(raw)))) }) } } From 0732389a0a368cdee7ad2ea6c3f0c22906355011 Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Thu, 24 Jul 2025 15:34:40 +0200 Subject: [PATCH 056/101] Docs(image-renderer): Update for v4.0.x (#108595) --- .../setup-grafana/image-rendering/_index.md | 7 +- .../image-rendering/troubleshooting/index.md | 66 +++++-------------- 2 files changed, 24 insertions(+), 49 deletions(-) diff --git a/docs/sources/setup-grafana/image-rendering/_index.md b/docs/sources/setup-grafana/image-rendering/_index.md index 43c45bf19e5..568f2d4e6a9 100644 --- a/docs/sources/setup-grafana/image-rendering/_index.md +++ b/docs/sources/setup-grafana/image-rendering/_index.md @@ -59,11 +59,16 @@ You can update your settings by using a configuration file, see [default.json](h You can volume mount your custom configuration file when starting the docker container: ```bash -docker run -d --name=renderer --network=host -v /some/path/config.json:/usr/src/app/config.json grafana/grafana-image-renderer:latest +docker run -d --name=renderer --network=host -v /some/path/config.json:/home/nonroot/config.json grafana/grafana-image-renderer:latest ``` You can see a docker-compose example using a custom configuration file [here](https://github.com/grafana/grafana-image-renderer/tree/master/devenv/docker/custom-config). +{{< admonition type="note" >}} +The configuration files were located in `/usr/src/app` up until v4.0.0 and later. +After this point, they are located in `/home/nonroot`. +{{< /admonition >}} + ### Security {{< admonition type="note" >}} diff --git a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md index 3c33d01dbc3..a2f472bce26 100644 --- a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md +++ b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md @@ -61,49 +61,8 @@ ldd chrome-headless-shell/linux-132.0.6781.0/chrome-headless-shell-linux64/chrom ... ``` -**Ubuntu:** - -On Ubuntu 18.10 the following dependencies are required for the image rendering to function. - -```bash -libx11-6 libx11-xcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrender1 libxtst6 libglib2.0-0 libnss3 libcups2 libdbus-1-3 libxss1 libxrandr2 libgtk-3-0 libasound2 libxcb-dri3-0 libgbm1 libxshmfence1 -``` - -**Debian:** - -On Debian 9 (Stretch) the following dependencies are required for the image rendering to function. - -```bash -libx11 libcairo libcairo2 libxtst6 libxcomposite1 libx11-xcb1 libxcursor1 libxdamage1 libnss3 libcups libcups2 libxss libxss1 libxrandr2 libasound2 libatk1.0-0 libatk-bridge2.0-0 libpangocairo-1.0-0 libgtk-3-0 libgbm1 libxshmfence1 -``` - -On Debian 10 (Buster) the following dependencies are required for the image rendering to function. - -```bash -libxdamage1 libxext6 libxi6 libxtst6 libnss3 libcups2 libxss1 libxrandr2 libasound2 libatk1.0-0 libatk-bridge2.0-0 libpangocairo-1.0-0 libpango-1.0-0 libcairo2 libatspi2.0-0 libgtk3.0-cil libgdk3.0-cil libx11-xcb-dev libgbm1 libxshmfence1 -``` - -**Centos:** - -On a minimal CentOS 7 installation, the following dependencies are required for the image rendering to function: - -```bash -libXcomposite libXdamage libXtst cups libXScrnSaver pango atk adwaita-cursor-theme adwaita-icon-theme at at-spi2-atk at-spi2-core cairo-gobject colord-libs dconf desktop-file-utils ed emacs-filesystem gdk-pixbuf2 glib-networking gnutls gsettings-desktop-schemas gtk-update-icon-cache gtk3 hicolor-icon-theme jasper-libs json-glib libappindicator-gtk3 libdbusmenu libdbusmenu-gtk3 libepoxy liberation-fonts liberation-narrow-fonts liberation-sans-fonts liberation-serif-fonts libgusb libindicator-gtk3 libmodman libproxy libsoup libwayland-cursor libwayland-egl libxkbcommon m4 mailx nettle patch psmisc redhat-lsb-core redhat-lsb-submod-security rest spax time trousers xdg-utils xkeyboard-config alsa-lib -``` - -On a minimal CentOS 8 installation, the following dependencies are required for the image rendering to function: - -```bash -libXcomposite libXdamage libXtst cups libXScrnSaver pango atk adwaita-cursor-theme adwaita-icon-theme at at-spi2-atk at-spi2-core cairo-gobject colord-libs dconf desktop-file-utils ed emacs-filesystem gdk-pixbuf2 glib-networking gnutls gsettings-desktop-schemas gtk-update-icon-cache gtk3 hicolor-icon-theme jasper-libs json-glib libappindicator-gtk3 libdbusmenu libdbusmenu-gtk3 libepoxy liberation-fonts liberation-narrow-fonts liberation-sans-fonts liberation-serif-fonts libgusb libindicator-gtk3 libmodman libproxy libsoup libwayland-cursor libwayland-egl libxkbcommon m4 mailx nettle patch psmisc redhat-lsb-core redhat-lsb-submod-security rest spax time trousers xdg-utils xkeyboard-config alsa-lib libX11-xcb -``` - -**RHEL:** - -On a minimal RHEL 8 installation, the following dependencies are required for the image rendering to function: - -```bash -linux-vdso.so.1 libdl.so.2 libpthread.so.0 libgobject-2.0.so.0 libglib-2.0.so.0 libnss3.so libnssutil3.so libsmime3.so libnspr4.so libatk-1.0.so.0 libatk-bridge-2.0.so.0 libcups.so.2 libgio-2.0.so.0 libdrm.so.2 libdbus-1.so.3 libexpat.so.1 libxcb.so.1 libxkbcommon.so.0 libm.so.6 libX11.so.6 libXcomposite.so.1 libXdamage.so.1 libXext.so.6 libXfixes.so.3 libXrandr.so.2 libgbm.so.1 libpango-1.0.so.0 libcairo.so.2 libasound.so.2 libatspi.so.0 libgcc_s.so.1 libc.so.6 /lib64/ld-linux-x86-64.so.2 libgnutls.so.30 libpcre.so.1 libffi.so.6 libplc4.so libplds4.so librt.so.1 libgmodule-2.0.so.0 libgssapi_krb5.so.2 libkrb5.so.3 libk5crypto.so.3 libcom_err.so.2 libavahi-common.so.3 libavahi-client.so.3 libcrypt.so.1 libz.so.1 libselinux.so.1 libresolv.so.2 libmount.so.1 libsystemd.so.0 libXau.so.6 libXrender.so.1 libthai.so.0 libfribidi.so.0 libpixman-1.so.0 libfontconfig.so.1 libpng16.so.16 libxcb-render.so.0 libidn2.so.0 libunistring.so.2 libtasn1.so.6 libnettle.so.6 libhogweed.so.4 libgmp.so.10 libkrb5support.so.0 libkeyutils.so.1 libpcre2-8.so.0 libuuid.so.1 liblz4.so.1 libgcrypt.so.20 libbz2.so.1 -``` +You can find a reference to all the relevant Debian packages for the service to function [in the Dockerfile](https://github.com/grafana/grafana-image-renderer/blob/master/Dockerfile). +If you are using an operating system that is not Debian 12, you should look up what each of those packages are called on your system. ## Certificate signed by internal certificate authorities @@ -127,6 +86,9 @@ If this happens, then you have to add the certificate to the trust store. If you [root@server ~]# chown -R grafana: /usr/share/grafana/.pki/nssdb ``` +You may also have to use other tooling than `certutil`, such as `update-ca-certificates` and its accompanying paths. +This depends on the Linux distribution you use; distributions often have a wiki with this type of information. + **Windows:** ``` @@ -138,17 +100,25 @@ certutil –addstore "Root" /internal-root-ca.crt.pem ```Dockerfile FROM grafana/grafana-image-renderer:latest +# Elevate our permissions to access system resources. USER root -RUN apk add --no-cache nss-tools +RUN mkdir -p /usr/local/share/ca-certificates/ +# Convert from .pem to .crt +RUN openssl x509 -inform PEM -in rootCA.pem -out /usr/local/share/ca-certificates/rootCA.crt -USER grafana +# Regenerate the CA certificates in the container. +RUN update-ca-certificates --fresh -COPY internal-root-ca.crt.pem /etc/pki/tls/certs/internal-root-ca.crt.pem -RUN mkdir -p /home/grafana/.pki/nssdb -RUN certutil -d sql:/home/grafana/.pki/nssdb -A -n internal-root-ca -t C -i /etc/pki/tls/certs/internal-root-ca.crt.pem +# Reassume the nonroot user for the service execution. +USER nonroot ``` +{{< admonition type="note" >}} +The container image was based on Alpine until v4.0.0. +After this point, it is based on distroless Debian. +{{< /admonition >}} + ## Custom Chrome/Chromium As a last resort, if you already have [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/) From 80bf664c485f22515dfa59916b025a94e2d2faff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:48:00 +0100 Subject: [PATCH 057/101] Update dependency @grafana/assistant to v0.0.8 (#108592) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 299f992444a..049a37d4a2c 100644 --- a/package.json +++ b/package.json @@ -271,7 +271,7 @@ "@formatjs/intl-durationformat": "^0.7.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/alerting": "workspace:*", - "@grafana/assistant": "0.0.7", + "@grafana/assistant": "0.0.8", "@grafana/aws-sdk": "0.7.1", "@grafana/azure-sdk": "0.0.7", "@grafana/data": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 03c67130507..263a381d788 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3073,17 +3073,17 @@ __metadata: languageName: unknown linkType: soft -"@grafana/assistant@npm:0.0.7": - version: 0.0.7 - resolution: "@grafana/assistant@npm:0.0.7" +"@grafana/assistant@npm:0.0.8": + version: 0.0.8 + resolution: "@grafana/assistant@npm:0.0.8" peerDependencies: - "@grafana/data": ">=12.0.0" - "@grafana/runtime": ">=12.0.0" + "@grafana/data": ">=12.1.0" + "@grafana/runtime": ">=12.1.0" "@grafana/scenes": ">=5.41.0" - "@grafana/ui": ">=12.0.0" + "@grafana/ui": ">=12.1.0" react: ">=18.0.0" rxjs: ">=7.0.0" - checksum: 10/c83523a032cb748867f683e87ad5b00a04bb4918ae5c7aafe5587b418b73b769a92de948335f0d9055d06036004e7a9794722f627cab3111b076eb27423d4344 + checksum: 10/2fd503b727716cb5bd3d25f16a50e177e58c9ea96712b66e785f1f2348e4f211b2d2250d8542420458ac2fb2f4f474f403850326bef9a99895990f63d009c640 languageName: node linkType: hard @@ -17862,7 +17862,7 @@ __metadata: "@formatjs/intl-durationformat": "npm:^0.7.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/alerting": "workspace:*" - "@grafana/assistant": "npm:0.0.7" + "@grafana/assistant": "npm:0.0.8" "@grafana/aws-sdk": "npm:0.7.1" "@grafana/azure-sdk": "npm:0.0.7" "@grafana/data": "workspace:*" From c81de5ee670920e305754ed476b0583bd60abc52 Mon Sep 17 00:00:00 2001 From: Anna Urbiztondo Date: Thu, 24 Jul 2025 16:00:09 +0200 Subject: [PATCH 058/101] Updating trace link (#108593) As reported in https://github.com/grafana/grafana/issues/101443. --- docs/sources/explore/get-started-with-explore.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/explore/get-started-with-explore.md b/docs/sources/explore/get-started-with-explore.md index d42862432d4..fbffb4bb769 100644 --- a/docs/sources/explore/get-started-with-explore.md +++ b/docs/sources/explore/get-started-with-explore.md @@ -204,5 +204,5 @@ Now that you are familiar with Explore you can: - [Build dashboards](https://grafana.com/docs/grafana//dashboards/build-dashboards/) - Create a wide variety of [visualizations](https://grafana.com/docs/grafana//panels-visualizations/visualizations/) - [Work with logs](https://grafana.com/docs/grafana//explore/logs-integration/) -- [Work with traces](https://grafana.com/docs/grafana/) +- [Work with traces](https://grafana.com/docs/grafana//explore/trace-integration/) - [Create and use correlations](https://grafana.com/docs/grafana//explore/correlations-editor-in-explore/) From 97c37e679825c85bee6564f3fbfb591cf264b43d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:14:26 +0000 Subject: [PATCH 059/101] Update dependency react-hook-form to v7.61.1 (#108598) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 263a381d788..404465af985 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26334,11 +26334,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.49.2": - version: 7.60.0 - resolution: "react-hook-form@npm:7.60.0" + version: 7.61.1 + resolution: "react-hook-form@npm:7.61.1" peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - checksum: 10/cc86ef4029f7b06f82dff919c8933156683b70669a6306d2471706443192b8916afaa07bc64452b590ecb18ea94f3d156f3fb38d59ada3462f5b108e89e186c9 + checksum: 10/961c5ad1f6fe694a66c1aa9934e5cd7ef014a8fd0d4ed15ac6d97d31d8d53b9e6196cb8b66d119883f49508c11589bc4b1478e1080790eb2e91c476551c82186 languageName: node linkType: hard From 36b0e5990518ea2f2e14099614e20b7dbbb5325d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 24 Jul 2025 16:15:09 +0200 Subject: [PATCH 060/101] codeowners: core-services will own pkg/services/query (#108599) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cdf2879eefd..caf829a42e3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -159,7 +159,7 @@ /pkg/services/preference/ @grafana/grafana-backend-group /pkg/services/provisioning/ @grafana/grafana-search-and-storage /pkg/services/provisioning/alerting/ @grafana/alerting-backend -/pkg/services/query/ @grafana/grafana-app-platform-squad +/pkg/services/query/ @grafana/grafana-datasources-core-services /pkg/services/queryhistory/ @grafana/observability-traces-and-profiling /pkg/services/quota/ @grafana/grafana-search-and-storage /pkg/services/screenshot/ @grafana/grafana-backend-group From e321ac1075a5a975dfed08f90ab7ab9539b5a048 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:39:39 +0000 Subject: [PATCH 061/101] Update dependency @babel/runtime to v7.28.2 (#108603) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 049a37d4a2c..36ebdd2b37b 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@arethetypeswrong/cli": "^0.18.2", "@babel/core": "7.28.0", "@babel/preset-env": "7.28.0", - "@babel/runtime": "7.27.6", + "@babel/runtime": "7.28.2", "@betterer/betterer": "5.4.0", "@betterer/cli": "5.4.0", "@crowdin/crowdin-api-client": "^1.42.0", diff --git a/yarn.lock b/yarn.lock index 404465af985..ef2b85d416f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,10 +1477,10 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:7.27.6, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": - version: 7.27.6 - resolution: "@babel/runtime@npm:7.27.6" - checksum: 10/cc957a12ba3781241b83d528eb69ddeb86ca5ac43179a825e83aa81263a6b3eb88c57bed8a937cdeacfc3192e07ec24c73acdfea4507d0c0428c8e23d6322bfe +"@babel/runtime@npm:7.28.2, @babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.5, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.25.6, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.7, @babel/runtime@npm:^7.27.0, @babel/runtime@npm:^7.27.6, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.7": + version: 7.28.2 + resolution: "@babel/runtime@npm:7.28.2" + checksum: 10/a0965fbdd6aaa40709290923bbe05e1c4314021f0cef608eb1d69f04f717c41829e50a53d79c4a0f461512b4be9b3c0190dc19387b219bcdaacdd793b2fe1b8a languageName: node linkType: hard @@ -17848,7 +17848,7 @@ __metadata: "@arethetypeswrong/cli": "npm:^0.18.2" "@babel/core": "npm:7.28.0" "@babel/preset-env": "npm:7.28.0" - "@babel/runtime": "npm:7.27.6" + "@babel/runtime": "npm:7.28.2" "@betterer/betterer": "npm:5.4.0" "@betterer/cli": "npm:5.4.0" "@bsull/augurs": "npm:^0.10.0" From 4def378a61986668ac4676d6b1cf355b2ad1ef0e Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 24 Jul 2025 17:59:48 +0300 Subject: [PATCH 062/101] Provisioning: Consolidate status display (#108589) * Provisioning: Centralize wizard errors * Move alert to shared * Update pull status * betterer * Update warning * Fix i18n * Format * Format[2] * Extract MessageList * Add fallback status --- .betterer.results | 12 --- .../provisioning/Job/FinishedJobStatus.tsx | 42 ++++++--- .../features/provisioning/Job/JobContent.tsx | 86 +++++++++++-------- .../features/provisioning/Job/JobStatus.tsx | 9 +- .../features/provisioning/Job/RecentJobs.tsx | 21 +---- .../Repository/RepositoryCard.tsx | 2 +- .../Repository/RepositoryOverview.tsx | 17 ++-- .../provisioning/Shared/MessageList.tsx | 28 ++++++ .../provisioning/Shared/ProvisioningAlert.tsx | 51 +++++++++++ .../provisioning/Shared/RepositoryList.tsx | 2 +- .../Wizard/ProvisioningWizard.tsx | 19 ++-- .../provisioning/Wizard/StepStatusContext.tsx | 2 + .../app/features/provisioning/Wizard/types.ts | 8 +- public/app/features/provisioning/types.ts | 5 ++ public/locales/en-US/grafana.json | 11 ++- 15 files changed, 207 insertions(+), 108 deletions(-) create mode 100644 public/app/features/provisioning/Shared/MessageList.tsx create mode 100644 public/app/features/provisioning/Shared/ProvisioningAlert.tsx diff --git a/.betterer.results b/.betterer.results index f4cd2232eef..4b110d17c26 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2653,18 +2653,6 @@ exports[`better eslint`] = { "public/app/features/provisioning/GettingStarted/SidebarItem.tsx:5381": [ [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] ], - "public/app/features/provisioning/Job/RecentJobs.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] - ], - "public/app/features/provisioning/Repository/RepositoryCard.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"] - ], - "public/app/features/provisioning/Repository/RepositoryOverview.tsx:5381": [ - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"], - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"], - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "2"], - [0, 0, 0, "Add noMargin prop to Card components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "3"] - ], "public/app/features/query/components/QueryEditorRow.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/public/app/features/provisioning/Job/FinishedJobStatus.tsx b/public/app/features/provisioning/Job/FinishedJobStatus.tsx index b5fd32c4353..9f633447433 100644 --- a/public/app/features/provisioning/Job/FinishedJobStatus.tsx +++ b/public/app/features/provisioning/Job/FinishedJobStatus.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react'; import { Trans, t } from '@grafana/i18n'; -import { Alert, Spinner, Stack, Text } from '@grafana/ui'; +import { Spinner, Stack, Text } from '@grafana/ui'; import { useGetRepositoryJobsWithPathQuery } from 'app/api/clients/provisioning/v0alpha1'; import { useStepStatus } from '../Wizard/StepStatusContext'; @@ -36,15 +36,26 @@ export function FinishedJobStatus({ jobUid, repositoryName }: FinishedJobProps) } if (finishedQuery.isSuccess && job?.status) { - if (job.status.state === 'error') { + const { state, message, errors } = job.status; + + if (state === 'error') { setStepStatusInfo({ status: 'error', + error: { + title: t('provisioning.job-status.status.title-error-running-job', 'Error running job'), + message: errors?.length ? errors : message, + }, }); - } else if (job.status.state === 'success') { - setStepStatusInfo({ status: 'success' }); - } else if (job.status.state === 'warning') { - // We treat warnings as success for now, but this could be changed later + } else if (state === 'success') { setStepStatusInfo({ status: 'success' }); + } else if (state === 'warning') { + setStepStatusInfo({ + status: 'warning', + warning: { + title: t('provisioning.job-status.status.title-warning-running-job', 'Job completed with warnings'), + message: errors?.length ? errors : message, + }, + }); } } @@ -56,14 +67,17 @@ export function FinishedJobStatus({ jobUid, repositoryName }: FinishedJobProps) }, [finishedQuery, job, setStepStatusInfo]); if (retryFailed) { - setStepStatusInfo({ status: 'error' }); - return ( - - - The job may have been deleted or could not be retrieved. Cancel the current process and start again. - - - ); + setStepStatusInfo({ + status: 'error', + error: { + title: t('provisioning.job-status.no-job-found', 'No job found'), + message: t( + 'provisioning.job-status.no-job-found-message', + 'The job may have been deleted or could not be retrieved. Cancel the current process and start again.' + ), + }, + }); + return null; } if (!job || finishedQuery.isLoading || finishedQuery.isFetching) { diff --git a/public/app/features/provisioning/Job/JobContent.tsx b/public/app/features/provisioning/Job/JobContent.tsx index 841800f2f7f..867f7844fda 100644 --- a/public/app/features/provisioning/Job/JobContent.tsx +++ b/public/app/features/provisioning/Job/JobContent.tsx @@ -1,9 +1,12 @@ +import { useEffect, useRef } from 'react'; + import { Trans, t } from '@grafana/i18n'; -import { Alert, ControlledCollapse, Spinner, Stack, Text } from '@grafana/ui'; +import { ControlledCollapse, Spinner, Stack, Text } from '@grafana/ui'; import { Job } from 'app/api/clients/provisioning/v0alpha1'; import { RepositoryLink } from '../Repository/RepositoryLink'; import ProgressBar from '../Shared/ProgressBar'; +import { useStepStatus } from '../Wizard/StepStatusContext'; import { JobSummary } from './JobSummary'; @@ -13,6 +16,9 @@ export interface JobContentProps { } export function JobContent({ job, isFinishedJob = false }: JobContentProps) { + const { setStepStatusInfo } = useStepStatus(); + const errorSetRef = useRef(false); + if (!job?.status) { return null; } @@ -20,48 +26,60 @@ export function JobContent({ job, isFinishedJob = false }: JobContentProps) { const { state, message, progress, summary, errors } = job.status; const repoName = job.metadata?.labels?.['provisioning.grafana.app/repository']; - const getStatusDisplay = () => { + // Update step status based on job state + useEffect(() => { + if (!state) { + return; + } + switch (state) { case 'success': - return ( - - ); + setStepStatusInfo({ status: 'success' }); + break; case 'warning': - return ( - - {errors?.length ? errors?.join('\n') : message} - - ); + if (!errorSetRef.current) { + setStepStatusInfo({ + status: 'warning', + warning: { + title: t('provisioning.job-status.status.title-warning-running-job', 'Job completed with warnings'), + message: errors?.length ? errors : message, + }, + }); + errorSetRef.current = true; + } + break; case 'error': - return ( - - {errors?.length ? errors?.join('\n') : message} - - ); + if (!errorSetRef.current) { + setStepStatusInfo({ + status: 'error', + error: { + title: t('provisioning.job-status.status.title-error-running-job', 'Error running job'), + message: errors?.length ? errors : message, + }, + }); + errorSetRef.current = true; + } + break; + case 'working': + case 'pending': + setStepStatusInfo({ status: 'running' }); + break; + default: + break; } - return ( - - {['working', 'pending'].includes(state ?? '') && } - - {message ?? state ?? ''} - - - ); - }; + }, [state, message, errors, setStepStatusInfo]); return ( - {getStatusDisplay()} + {['working', 'pending'].includes(state ?? '') && ( + + + + {message ?? state ?? t('provisioning.job-status.starting', 'Starting...')} + + + )} {state && !['success', 'error'].includes(state) && ( diff --git a/public/app/features/provisioning/Job/JobStatus.tsx b/public/app/features/provisioning/Job/JobStatus.tsx index f003245e263..ee05d56c051 100644 --- a/public/app/features/provisioning/Job/JobStatus.tsx +++ b/public/app/features/provisioning/Job/JobStatus.tsx @@ -1,4 +1,4 @@ -import { Trans } from '@grafana/i18n'; +import { Trans, t } from '@grafana/i18n'; import { Spinner, Stack, Text } from '@grafana/ui'; import { Job, useListJobQuery } from 'app/api/clients/provisioning/v0alpha1'; @@ -36,7 +36,12 @@ export function JobStatus({ watch }: JobStatusProps) { } if (activeQuery.isError) { - setStepStatusInfo({ status: 'error', error: 'Error fetching active job' }); + setStepStatusInfo({ + status: 'error', + error: { + title: t('provisioning.job-status.title.error-fetching-active-job', 'Error fetching active job'), + }, + }); return null; } diff --git a/public/app/features/provisioning/Job/RecentJobs.tsx b/public/app/features/provisioning/Job/RecentJobs.tsx index cf1291bcb2f..550511b5069 100644 --- a/public/app/features/provisioning/Job/RecentJobs.tsx +++ b/public/app/features/provisioning/Job/RecentJobs.tsx @@ -2,10 +2,11 @@ import { useMemo } from 'react'; import { intervalToAbbreviatedDurationString, TraceKeyValuePair } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Alert, Badge, Box, Card, Icon, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui'; +import { Alert, Badge, Box, Card, InteractiveTable, Spinner, Stack, Text } from '@grafana/ui'; import { Job, Repository, SyncStatus } from 'app/api/clients/provisioning/v0alpha1'; import KeyValuesTable from 'app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable'; +import { ProvisioningAlert } from '../Shared/ProvisioningAlert'; import { useRepositoryAllJobs } from '../hooks/useRepositoryAllJobs'; import { formatTimestamp } from '../utils/time'; @@ -126,21 +127,7 @@ function ExpandedRow({ row }: ExpandedRowProps) { )} - {hasErrors && ( - - {row.status?.errors?.map( - (error, index) => - error.trim() && ( - - - - {error} - - - ) - )} - - )} + {hasErrors && } {hasSummary && ( @@ -212,7 +199,7 @@ export function RecentJobs({ repo }: Props) { } return ( - + Jobs diff --git a/public/app/features/provisioning/Repository/RepositoryCard.tsx b/public/app/features/provisioning/Repository/RepositoryCard.tsx index e2a022cebb5..e56f8655eb9 100644 --- a/public/app/features/provisioning/Repository/RepositoryCard.tsx +++ b/public/app/features/provisioning/Repository/RepositoryCard.tsx @@ -58,7 +58,7 @@ export function RepositoryCard({ repository }: Props) { }; return ( - + diff --git a/public/app/features/provisioning/Repository/RepositoryOverview.tsx b/public/app/features/provisioning/Repository/RepositoryOverview.tsx index ba18bf32fcf..b4f7bfd019a 100644 --- a/public/app/features/provisioning/Repository/RepositoryOverview.tsx +++ b/public/app/features/provisioning/Repository/RepositoryOverview.tsx @@ -6,6 +6,7 @@ import { Box, Card, CellProps, Grid, InteractiveTable, LinkButton, Stack, Text, import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1'; import { RecentJobs } from '../Job/RecentJobs'; +import { MessageList } from '../Shared/MessageList'; import { formatTimestamp } from '../utils/time'; import { CheckRepository } from './CheckRepository'; @@ -51,7 +52,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
- + Resources @@ -73,7 +74,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
{repo.status?.health && (
- + Health @@ -129,7 +130,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
)}
- + Pull status @@ -192,13 +193,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) {
- - {status.sync.message.map((msg, idx) => ( - - {msg} - - ))} - +
)} @@ -211,7 +206,7 @@ export function RepositoryOverview({ repo }: { repo: Repository }) { {repo.status?.webhook && (
- + Webhook diff --git a/public/app/features/provisioning/Shared/MessageList.tsx b/public/app/features/provisioning/Shared/MessageList.tsx new file mode 100644 index 00000000000..2d7f0f15f2f --- /dev/null +++ b/public/app/features/provisioning/Shared/MessageList.tsx @@ -0,0 +1,28 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Text, useStyles2 } from '@grafana/ui'; + +interface MessageListProps { + messages: string[]; + variant?: 'body' | 'bodySmall'; +} + +export function MessageList({ messages, variant }: MessageListProps) { + const styles = useStyles2(getStyles); + + return ( +
    + {messages.map((msg, index) => ( +
  • {variant ? {msg} : msg}
  • + ))} +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + messageList: css({ + margin: 0, + paddingLeft: theme.spacing(3), + }), +}); diff --git a/public/app/features/provisioning/Shared/ProvisioningAlert.tsx b/public/app/features/provisioning/Shared/ProvisioningAlert.tsx new file mode 100644 index 00000000000..94fb45423cc --- /dev/null +++ b/public/app/features/provisioning/Shared/ProvisioningAlert.tsx @@ -0,0 +1,51 @@ +import { t } from '@grafana/i18n'; +import { Alert } from '@grafana/ui'; + +import { ProvisioningErrorInfo } from '../types'; + +import { MessageList } from './MessageList'; + +interface ProvisioningAlertProps { + error?: string | ProvisioningErrorInfo; + warning?: string | ProvisioningErrorInfo; +} + +const getTitle = (alert: string | ProvisioningErrorInfo, isWarning = false) => { + if (typeof alert === 'string') { + return alert; + } + + if (isWarning) { + return alert.title || t('provisioning.warning-title-default', 'Warning'); + } else { + return alert.title || t('provisioning.error-title-default', 'Error'); + } +}; + +const getMessage = (alert: string | ProvisioningErrorInfo) => { + if (typeof alert === 'string' || !alert.message) { + return null; + } + + if (Array.isArray(alert.message)) { + return ; + } + + return alert.message; +}; + +export function ProvisioningAlert({ error, warning }: ProvisioningAlertProps) { + const alertData = error || warning; + const isWarning = Boolean(warning); + const severity = isWarning ? 'warning' : 'error'; + + if (!alertData) { + return null; + } + + return ( + + {getMessage(alertData)} + + ); +} diff --git a/public/app/features/provisioning/Shared/RepositoryList.tsx b/public/app/features/provisioning/Shared/RepositoryList.tsx index 95d42d28d10..368ac5b644d 100644 --- a/public/app/features/provisioning/Shared/RepositoryList.tsx +++ b/public/app/features/provisioning/Shared/RepositoryList.tsx @@ -30,7 +30,7 @@ export function RepositoryList({ items }: Props) { )} - + {filteredItems.length ? ( filteredItems.map((item) => ) ) : ( diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index edf8b5aea3e..6c9ef49e917 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -6,11 +6,12 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getAppEvents, isFetchError } from '@grafana/runtime'; -import { Alert, Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; import { getDefaultValues } from '../Config/defaults'; +import { ProvisioningAlert } from '../Shared/ProvisioningAlert'; import { PROVISIONING_URL } from '../constants'; import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository'; import { dataToSpec } from '../utils/data'; @@ -64,7 +65,8 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { const [isSubmitting, setIsSubmitting] = useState(false); const [isCancelling, setIsCancelling] = useState(false); - const { stepStatusInfo, setStepStatusInfo, isStepSuccess, isStepRunning, hasStepError } = useStepStatus(); + const { stepStatusInfo, setStepStatusInfo, isStepSuccess, isStepRunning, hasStepError, hasStepWarning } = + useStepStatus(); const { data } = useGetFrontendSettingsQuery(); const isLegacyStorage = Boolean(data?.legacyStorage); @@ -250,8 +252,8 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { setIsSubmitting(false); } } else { - // only proceed if the job was successful - if (isStepSuccess) { + // proceed if the job was successful or had warnings + if (isStepSuccess || hasStepWarning) { handleNext(); } } @@ -262,9 +264,9 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { if (activeStep !== 'connection' && hasStepError) { return true; } - // Synchronize step requires success to proceed + // Synchronize step requires success or warning to proceed if (activeStep === 'synchronize') { - return !isStepSuccess; // Disable next button if the step is not successful + return !(isStepSuccess || hasStepWarning); // Disable next button if the step is not successful or has warnings } return isSubmitting || isCancelling || isStepRunning || isCreatingSkipJob; }; @@ -283,9 +285,8 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { - {hasStepError && 'error' in stepStatusInfo && stepStatusInfo.error && ( - - )} + {hasStepError && 'error' in stepStatusInfo && } + {hasStepWarning && 'warning' in stepStatusInfo && }
{activeStep === 'connection' && } diff --git a/public/app/features/provisioning/Wizard/StepStatusContext.tsx b/public/app/features/provisioning/Wizard/StepStatusContext.tsx index e3081bfba02..efd86869979 100644 --- a/public/app/features/provisioning/Wizard/StepStatusContext.tsx +++ b/public/app/features/provisioning/Wizard/StepStatusContext.tsx @@ -11,6 +11,7 @@ interface StepStatusContextData { // Computed status checks hasStepError: boolean; + hasStepWarning: boolean; isStepRunning: boolean; isStepSuccess: boolean; isStepIdle: boolean; @@ -29,6 +30,7 @@ export const StepStatusProvider = ({ children }: PropsWithChildren) => { stepStatusInfo, setStepStatusInfo, hasStepError: stepStatusInfo.status === 'error', + hasStepWarning: stepStatusInfo.status === 'warning', isStepRunning: stepStatusInfo.status === 'running', isStepSuccess: stepStatusInfo.status === 'success', isStepIdle: stepStatusInfo.status === 'idle', diff --git a/public/app/features/provisioning/Wizard/types.ts b/public/app/features/provisioning/Wizard/types.ts index 7b77e277ffa..fd9fcd00f1c 100644 --- a/public/app/features/provisioning/Wizard/types.ts +++ b/public/app/features/provisioning/Wizard/types.ts @@ -1,6 +1,6 @@ import { RepositorySpec, SyncOptions } from 'app/api/clients/provisioning/v0alpha1'; -import { RepositoryFormData } from '../types'; +import { ProvisioningErrorInfo, RepositoryFormData } from '../types'; export type WizardStep = 'connection' | 'bootstrap' | 'finish' | 'synchronize'; @@ -26,5 +26,7 @@ export interface ModeOption { subtitle: string; } -export type StepStatus = 'idle' | 'running' | 'error' | 'success'; -export type StepStatusInfo = { status: StepStatus } | { status: 'error'; error: string }; +export type StepStatusInfo = + | { status: 'idle' | 'running' | 'success' } + | { status: 'error'; error: string | ProvisioningErrorInfo } + | { status: 'warning'; warning: string | ProvisioningErrorInfo }; diff --git a/public/app/features/provisioning/types.ts b/public/app/features/provisioning/types.ts index 516beb0dc67..8f8ee2df385 100644 --- a/public/app/features/provisioning/types.ts +++ b/public/app/features/provisioning/types.ts @@ -89,3 +89,8 @@ export type HistoryListResponse = { metadata?: Record; items?: HistoryItem[]; }; + +export interface ProvisioningErrorInfo { + title?: string; + message?: string | string[]; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 8a70d10f52d..4de18dad330 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -11070,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Instant updates and pull requests with webhooks.", "title-visual-previews-in-pull-requests": "Visual previews in pull requests with image rendering" }, + "error-title-default": "Error", "expanded-row": { "job-specification": "Job Specification", - "summary": "Summary", - "title-error": "Error" + "summary": "Summary" }, "features-list": { "actions": { @@ -11218,10 +11218,12 @@ "starting": "Starting...", "status": { "title-error-running-job": "Error running job", - "title-job-completed-successfully": "Job completed successfully", "title-warning-running-job": "Job completed with warnings" }, - "summary": "Summary" + "summary": "Summary", + "title": { + "error-fetching-active-job": "Error fetching active job" + } }, "local": { "path-description": "Local file system path to the repository", @@ -11387,6 +11389,7 @@ "go-to": "Go to", "make-sure": "Make sure to include these permissions" }, + "warning-title-default": "Warning", "wizard": { "alert-point-1": "Resources won't be able to be created, edited, or deleted during this process. In the last step, they will disappear.", "alert-point-2": "Once provisioning is complete, resources will reappear and be managed through external storage.", From f0b0aa24a35237c2e58a3a39634af32fbe5f42b7 Mon Sep 17 00:00:00 2001 From: Anna Urbiztondo Date: Thu, 24 Jul 2025 17:01:58 +0200 Subject: [PATCH 063/101] Advisor - mark as public preview (#108605) As per https://github.com/grafana/grafana/issues/106893. --- docs/sources/administration/grafana-advisor/_index.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/sources/administration/grafana-advisor/_index.md b/docs/sources/administration/grafana-advisor/_index.md index 4284432b73b..38673e9e121 100644 --- a/docs/sources/administration/grafana-advisor/_index.md +++ b/docs/sources/administration/grafana-advisor/_index.md @@ -7,7 +7,6 @@ labels: - oss - cloud - enterprise - stage: experimental keywords: - grafana - grafana advisor @@ -17,7 +16,7 @@ keywords: # Grafana Advisor -{{< docs/experimental product="Grafana Advisor" featureFlag="grafanaAdvisor" >}} +{{< docs/public-preview product="Grafana Advisor" featureFlag="grafanaAdvisor" >}} ## Overview From 512a55372375da28b7d70dcdec61d542c02a5d2d Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Thu, 24 Jul 2025 08:10:12 -0700 Subject: [PATCH 064/101] Geomap: Add tooltip support for dynamic geojson (#108203) * Geomap: Add tooltip support for dynamic geojson * Add tests for tooltip function --- .betterer.results | 3 - .../geomap/layers/data/geojsonDynamic.test.ts | 271 ++++++++++++++++++ .../geomap/layers/data/geojsonDynamic.ts | 72 ++++- 3 files changed, 335 insertions(+), 11 deletions(-) create mode 100644 public/app/plugins/panel/geomap/layers/data/geojsonDynamic.test.ts diff --git a/.betterer.results b/.betterer.results index 4b110d17c26..f43114a1877 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3798,9 +3798,6 @@ exports[`better eslint`] = { "public/app/plugins/panel/geomap/layers/basemaps/esri.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/panel/geomap/layers/data/routeLayer.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], diff --git a/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.test.ts b/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.test.ts new file mode 100644 index 00000000000..613f9983fbf --- /dev/null +++ b/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.test.ts @@ -0,0 +1,271 @@ +import Feature from 'ol/Feature'; +import { Point, Polygon } from 'ol/geom'; +import VectorSource from 'ol/source/Vector'; + +import { FieldType, createDataFrame } from '@grafana/data'; + +import { updateFeaturePropertiesForTooltip } from './geojsonDynamic'; + +describe('Dynamic GeoJSON Layer', () => { + describe('updateFeaturePropertiesForTooltip', () => { + let source: VectorSource; + let idToIdx: Map; + let mockFeature1: Feature; + let mockFeature2: Feature; + let mockFeature3: Feature; + let forEachFeatureSpy: jest.SpyInstance; + + beforeEach(() => { + source = new VectorSource(); + idToIdx = new Map(); + + // Create mock features with different IDs + mockFeature1 = new Feature({ + geometry: new Point([0, 0]), + }); + mockFeature1.setId('feature1'); + + mockFeature2 = new Feature({ + geometry: new Polygon([[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]), + }); + mockFeature2.setId('feature2'); + + mockFeature3 = new Feature({ + geometry: new Point([2, 2]), + }); + mockFeature3.setId('feature3'); + + // Add features to source + source.addFeature(mockFeature1); + source.addFeature(mockFeature2); + source.addFeature(mockFeature3); + + // Set up spy for forEachFeature method + forEachFeatureSpy = jest.spyOn(source, 'forEachFeature'); + }); + + afterEach(() => { + source.clear(); + idToIdx.clear(); + forEachFeatureSpy.mockRestore(); + }); + + it('should update feature properties when frame and idField are provided', () => { + const frame = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['feature1', 'feature2', 'feature3'] }, + { name: 'value', type: FieldType.number, values: [10, 20, 30] }, + { name: 'name', type: FieldType.string, values: ['First', 'Second', 'Third'] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, 'id', idToIdx); + + // Check that idToIdx map is populated correctly + expect(idToIdx.get('feature1')).toBe(0); + expect(idToIdx.get('feature2')).toBe(1); + expect(idToIdx.get('feature3')).toBe(2); + + // Check that features have frame and rowIndex properties set + expect(mockFeature1.get('frame')).toBe(frame); + expect(mockFeature1.get('rowIndex')).toBe(0); + + expect(mockFeature2.get('frame')).toBe(frame); + expect(mockFeature2.get('rowIndex')).toBe(1); + + expect(mockFeature3.get('frame')).toBe(frame); + expect(mockFeature3.get('rowIndex')).toBe(2); + }); + + it('should handle partial matches between features and frame data', () => { + const frame = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['feature1', 'feature3'] }, // missing feature2 + { name: 'value', type: FieldType.number, values: [10, 30] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, 'id', idToIdx); + + // Check that only matching features have properties set + expect(mockFeature1.get('frame')).toBe(frame); + expect(mockFeature1.get('rowIndex')).toBe(0); + + expect(mockFeature2.get('frame')).toBeUndefined(); + expect(mockFeature2.get('rowIndex')).toBeUndefined(); + + expect(mockFeature3.get('frame')).toBe(frame); + expect(mockFeature3.get('rowIndex')).toBe(1); + }); + + it('should handle features with no matching data', () => { + const frame = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['nonexistent1', 'nonexistent2'] }, + { name: 'value', type: FieldType.number, values: [10, 20] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, 'id', idToIdx); + + // Check that no features have properties set since IDs don't match + expect(mockFeature1.get('frame')).toBeUndefined(); + expect(mockFeature1.get('rowIndex')).toBeUndefined(); + + expect(mockFeature2.get('frame')).toBeUndefined(); + expect(mockFeature2.get('rowIndex')).toBeUndefined(); + + expect(mockFeature3.get('frame')).toBeUndefined(); + expect(mockFeature3.get('rowIndex')).toBeUndefined(); + }); + + it('should clear idToIdx map on each call', () => { + // First call + const frame1 = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['feature1'] }, + { name: 'value', type: FieldType.number, values: [10] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame1, 'id', idToIdx); + expect(idToIdx.size).toBe(1); + expect(idToIdx.get('feature1')).toBe(0); + + // Second call with different data + const frame2 = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['feature2', 'feature3'] }, + { name: 'value', type: FieldType.number, values: [20, 30] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame2, 'id', idToIdx); + + // Map should be cleared and repopulated + expect(idToIdx.size).toBe(2); + expect(idToIdx.get('feature1')).toBeUndefined(); + expect(idToIdx.get('feature2')).toBe(0); + expect(idToIdx.get('feature3')).toBe(1); + }); + + it('should handle features with null or undefined IDs', () => { + // Add a feature with no ID + const featureWithoutId = new Feature({ + geometry: new Point([3, 3]), + }); + source.addFeature(featureWithoutId); + + const frame = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['feature1', 'feature2'] }, + { name: 'value', type: FieldType.number, values: [10, 20] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, 'id', idToIdx); + + // Features with IDs should be processed + expect(mockFeature1.get('frame')).toBe(frame); + expect(mockFeature1.get('rowIndex')).toBe(0); + + // Feature without ID should not be processed + expect(featureWithoutId.get('frame')).toBeUndefined(); + expect(featureWithoutId.get('rowIndex')).toBeUndefined(); + }); + + it('should handle string and number IDs', () => { + // Add features with number IDs + const featureWithNumberId = new Feature({ + geometry: new Point([4, 4]), + }); + featureWithNumberId.setId(123); + source.addFeature(featureWithNumberId); + + const frame = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.number, values: [123, 456] }, + { name: 'value', type: FieldType.number, values: [100, 200] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, 'id', idToIdx); + + // Feature with number ID should be processed (converted to string) + expect(featureWithNumberId.get('frame')).toBe(frame); + expect(featureWithNumberId.get('rowIndex')).toBe(0); + }); + + it('should return early when frame is undefined', () => { + updateFeaturePropertiesForTooltip(source, undefined, 'id', idToIdx); + + expect(forEachFeatureSpy).not.toHaveBeenCalled(); + expect(idToIdx.size).toBe(0); + }); + + it('should return early when idField is undefined', () => { + const frame = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['feature1'] }, + { name: 'value', type: FieldType.number, values: [10] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, undefined, idToIdx); + + expect(forEachFeatureSpy).not.toHaveBeenCalled(); + expect(idToIdx.size).toBe(0); + }); + + it('should return early when idField is empty string', () => { + const frame = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['feature1'] }, + { name: 'value', type: FieldType.number, values: [10] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, '', idToIdx); + + expect(forEachFeatureSpy).not.toHaveBeenCalled(); + expect(idToIdx.size).toBe(0); + }); + + it('should handle when idField does not exist in frame', () => { + const frame = createDataFrame({ + fields: [ + { name: 'different_field', type: FieldType.string, values: ['feature1'] }, + { name: 'value', type: FieldType.number, values: [10] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, 'nonexistent_field', idToIdx); + + expect(forEachFeatureSpy).not.toHaveBeenCalled(); + expect(idToIdx.size).toBe(0); + }); + + it('should preserve existing feature properties while adding tooltip properties', () => { + // Set some existing properties on the feature + mockFeature1.set('existingProperty', 'existing value'); + mockFeature1.set('anotherProperty', 42); + + const frame = createDataFrame({ + fields: [ + { name: 'id', type: FieldType.string, values: ['feature1'] }, + { name: 'value', type: FieldType.number, values: [10] }, + ], + }); + + updateFeaturePropertiesForTooltip(source, frame, 'id', idToIdx); + + // Check that existing properties are preserved + expect(mockFeature1.get('existingProperty')).toBe('existing value'); + expect(mockFeature1.get('anotherProperty')).toBe(42); + + // Check that tooltip properties are added + expect(mockFeature1.get('frame')).toBe(frame); + expect(mockFeature1.get('rowIndex')).toBe(0); + }); + }); +}); diff --git a/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts b/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts index b09aa0a2576..53bab286a52 100644 --- a/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts +++ b/public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts @@ -15,6 +15,7 @@ import { GrafanaTheme2, PluginState, EventBus, + DataFrame, } from '@grafana/data'; import { ComparisonOperation } from '@grafana/schema'; import { findField } from 'app/features/dimensions/utils'; @@ -65,6 +66,14 @@ export const DEFAULT_STYLE_RULE: FeatureStyleConfig = { }, }; +// Default configuration with tooltip support enabled +export const defaultDynamicGeoJSONConfig: MapLayerOptions = { + type: 'dynamic-geojson', + name: 'Dynamic GeoJSON', + config: defaultOptions, + tooltip: true, +}; + export const dynamicGeoJSONLayer: MapLayerRegistryItem = { id: 'dynamic-geojson', name: 'Dynamic GeoJSON', @@ -88,11 +97,20 @@ export const dynamicGeoJSONLayer: MapLayerRegistryItem(); + // Function to update feature properties for tooltip support + const updateFeatureProperties = (frame?: DataFrame) => { + updateFeaturePropertiesForTooltip(source, frame, config.idField, idToIdx); + }; + const key = source.on('change', () => { //one geojson loads if (source.getState() === 'ready') { unByKey(key); features.next(source.getFeatures()); + // Apply current data to newly loaded features + if (currentFrame) { + updateFeatureProperties(currentFrame); + } } }); @@ -117,14 +135,16 @@ export const dynamicGeoJSONLayer: MapLayerRegistryItem(); + let currentFrame: DataFrame | undefined = undefined; const vectorLayer = new VectorImage({ source, style: (feature: FeatureLike) => { - const idx = idToIdx.get(feature.getId() as string); + const featureId = feature.getId(); + const idx = featureId != null ? idToIdx.get(String(featureId)) : undefined; const dims = style.dims; - if (idx && dims) { + if (idx !== undefined && dims) { return new Style({ fill: new Fill({ color: dims.color?.get(idx) }), stroke: new Stroke({ color: style.base.color, width: style.base.lineWidth ?? 1 }), @@ -174,12 +194,12 @@ export const dynamicGeoJSONLayer: MapLayerRegistryItem { const frame = data.series[0]; if (frame) { - const field = findField(frame, config.idField); - if (field) { - idToIdx.clear(); - field.values.forEach((v, i) => idToIdx.set(v, i)); - } - + currentFrame = frame; + + // Update feature properties for tooltip support + updateFeatureProperties(frame); + + // Update style dimensions for data-driven styling style.dims = getStyleDimension(frame, style, theme, config.dataStyle); } vectorLayer.changed(); @@ -232,3 +252,39 @@ export const dynamicGeoJSONLayer: MapLayerRegistryItem +): void { + if (!frame || !idField) { + return; + } + + const field = findField(frame, idField); + if (field) { + idToIdx.clear(); + field.values.forEach((v, i) => idToIdx.set(String(v), i)); + + source.forEachFeature((feature) => { + const featureId = feature.getId(); + if (featureId != null) { + const rowIndex = idToIdx.get(String(featureId)); + if (rowIndex !== undefined) { + // Set tooltip properties without overwriting existing GeoJSON properties + feature.set('frame', frame); + feature.set('rowIndex', rowIndex); + } + } + }); + } +} From 2be4e44a029dbbe8906252f6f4b5b4e60a9c874c Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 24 Jul 2025 16:32:02 +0100 Subject: [PATCH 065/101] Accessibility: Add storybook a11y tests that run in CI (#108543) * add test runner and start experimenting with storybook a11y tests * skip currently failing a11y tests * first attempt at github action * fix CODEOWNERS, fix copy assets * no caching? * make wait-on better * increase timeout and add verbose logging * kick CI * remove verbose logging, try different runner * remove commented out code in copyAssets * try maxWorkers 2 * turn on verbose logging, run as part of same step * test storybook verify * try setting LANG * tidy up * add betterer test --- .betterer.results | 158 +++ .betterer.ts | 24 +- .github/CODEOWNERS | 1 + .github/workflows/storybook-a11y.yml | 57 + package.json | 1 + .../ContactPointSelector.story.tsx | 2 + packages/grafana-ui/.storybook/copyAssets.ts | 15 +- packages/grafana-ui/.storybook/preview.ts | 1 + packages/grafana-ui/package.json | 2 + .../AutoSaveField/AutoSaveField.story.tsx | 2 + .../src/components/Button/Button.story.tsx | 2 + .../components/Carousel/Carousel.story.tsx | 2 + .../components/Cascader/Cascader.story.tsx | 2 + .../ColorPicker/ColorPicker.story.tsx | 2 + .../ColorPicker/ColorPickerInput.story.tsx | 2 + .../Combobox/MultiCombobox.story.tsx | 2 + .../ConfirmButton/ConfirmButton.story.tsx | 2 + .../ContextMenu/ContextMenu.story.tsx | 2 + .../DateTimePickers/TimeOfDayPicker.story.tsx | 2 + .../FileDropzone/FileDropzone.story.tsx | 2 + .../components/FormField/FormField.story.tsx | 2 + .../src/components/Forms/Checkbox.story.tsx | 2 + .../src/components/Forms/Field.story.tsx | 2 + .../src/components/Forms/FieldArray.story.tsx | 2 + .../src/components/Forms/FieldSet.story.tsx | 2 + .../src/components/Forms/Form.story.tsx | 2 + .../components/Forms/InlineField.story.tsx | 2 + .../RadioButtonGroup.story.tsx | 2 + .../InlineToast/InlineToast.story.tsx | 2 + .../src/components/Input/Input.story.tsx | 2 + .../InteractiveTable.story.tsx | 2 + .../src/components/Layout/Grid/Grid.story.tsx | 2 + .../components/Layout/Stack/Stack.story.tsx | 2 + .../src/components/Link/TextLink.story.tsx | 2 + .../LoadingBar/LoadingBar.story.tsx | 2 + .../src/components/Menu/Menu.story.tsx | 2 + .../src/components/Modal/Modal.story.tsx | 2 + .../PageLayout/PageToolbar.story.tsx | 5 +- .../PanelChrome/PanelChrome.story.tsx | 2 + .../QueryField/QueryField.story.tsx | 2 + .../ScrollContainer/ScrollContainer.story.tsx | 2 + .../src/components/Segment/Segment.story.tsx | 4 + .../components/Segment/SegmentAsync.story.tsx | 4 + .../components/Segment/SegmentInput.story.tsx | 4 + .../src/components/Select/Select.story.tsx | 2 + .../components/Select/SelectPerf.story.tsx | 4 + .../components/Slider/RangeSlider.story.tsx | 2 + .../src/components/Slider/Slider.story.tsx | 2 + .../StatsPicker/StatsPicker.story.tsx | 2 + .../src/components/Switch/Switch.story.tsx | 2 + .../src/components/Table/Table.story.tsx | 2 + .../TableInputCSV/TableInputCSV.story.tsx | 4 + .../src/components/Tags/TagList.story.tsx | 2 + .../src/components/Text/Text.story.tsx | 2 + .../components/ThemeDemos/ThemeDemo.story.tsx | 2 + .../ThemeDemos/Typography.story.tsx | 4 + .../ToolbarButton/ToolbarButton.story.tsx | 2 + .../components/VizLayout/VizLayout.story.tsx | 2 + .../components/VizLegend/VizLegend.story.tsx | 4 + yarn.lock | 1041 ++++++++++++++++- 60 files changed, 1353 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/storybook-a11y.yml diff --git a/.betterer.results b/.betterer.results index f43114a1877..86db65d8f44 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4226,6 +4226,164 @@ exports[`no undocumented stories`] = { }` }; +exports[`no skipping a11y tests in stories`] = { + value: `{ + "packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Button/Button.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Carousel/Carousel.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Cascader/Cascader.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/FormField/FormField.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Forms/Checkbox.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Forms/Field.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Forms/FieldArray.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Forms/FieldSet.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Forms/Form.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Forms/InlineField.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/InlineToast/InlineToast.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Input/Input.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Layout/Stack/Stack.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Link/TextLink.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/LoadingBar/LoadingBar.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Menu/Menu.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Modal/Modal.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/QueryField/QueryField.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Segment/Segment.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Select/Select.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Select/SelectPerf.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Slider/Slider.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Switch/Switch.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Table/Table.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Tags/TagList.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/Text/Text.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ], + "packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx:5381": [ + [0, 0, 0, "No skipping of a11y tests in stories. Please fix the component or story instead.", "5381"] + ] + }` +}; + exports[`no gf-form usage`] = { value: `{ "packages/grafana-prometheus/src/components/PromExploreExtraField.tsx:5381": [ diff --git a/.betterer.ts b/.betterer.ts index 61e6153b4dd..fe3b3c7f9f7 100644 --- a/.betterer.ts +++ b/.betterer.ts @@ -17,13 +17,35 @@ export default { countEslintErrors() .include('**/*.{ts,tsx}') .exclude(new RegExp(eslintPathsToIgnore.join('|'))), - 'no undocumented stories': () => countUndocumentedStories().include('**/!(*.internal).story.tsx'), + 'no undocumented stories': () => countUndocumentedStories().include('**/*.story.tsx'), + 'no skipping a11y tests in stories': () => countSkippedA11yTestStories().include('**/*.story.tsx'), 'no gf-form usage': () => regexp(/gf-form/gm, 'gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.') .include('**/*.{ts,tsx,html}') .exclude(new RegExp('packages/grafana-ui/src/themes/GlobalStyles')), }; +function countSkippedA11yTestStories() { + return new BettererFileTest(async (filePaths, fileTestResult) => { + await Promise.all( + filePaths.map(async (filePath) => { + // look for skipped a11y tests + const skipRegex = new RegExp("a11y: { test: 'off' }", 'gm'); + + const fileText = await fs.readFile(filePath, 'utf8'); + + const hasSkip = skipRegex.test(fileText); + if (hasSkip) { + // In this case the file contents don't matter: + const file = fileTestResult.addFile(filePath, ''); + // Add the issue to the first character of the file: + file.addIssue(0, 0, 'No skipping of a11y tests in stories. Please fix the component or story instead.'); + } + }) + ); + }); +} + function countUndocumentedStories() { return new BettererFileTest(async (filePaths, fileTestResult) => { await Promise.all( diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index caf829a42e3..c9d41fb8b7e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -999,6 +999,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/publish-technical-documentation-release.yml @grafana/docs-tooling /.github/workflows/scripts/json-file-to-job-output.js @grafana/plugins-platform-frontend /.github/workflows/stale.yml @grafana/grafana-developer-enablement-squad +/.github/workflows/storybook-a11y.yml @grafana/grafana-frontend-platform /.github/workflows/storybook-verification.yml @grafana/grafana-frontend-platform /.github/workflows/storybook-verification-playwright.yml @grafana/grafana-frontend-platform /.github/workflows/update-make-docs.yml @grafana/docs-tooling diff --git a/.github/workflows/storybook-a11y.yml b/.github/workflows/storybook-a11y.yml new file mode 100644 index 00000000000..f666ba37b95 --- /dev/null +++ b/.github/workflows/storybook-a11y.yml @@ -0,0 +1,57 @@ +name: Run Storybook a11y tests +on: + pull_request: + push: + branches: + - main + - release-*.*.* + +permissions: {} + +jobs: + detect-changes: + name: Detect whether code changed + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + changed: ${{ steps.detect-changes.outputs.frontend }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: true # required to get more history in the changed-files action + fetch-depth: 2 + - name: Detect changes + id: detect-changes + uses: ./.github/actions/change-detection + with: + self: .github/workflows/storybook-a11y.yml + + test-storybook-a11y: + runs-on: ubuntu-latest-8-cores + permissions: + contents: read + id-token: write + needs: detect-changes + if: needs.detect-changes.outputs.changed == 'true' + name: "Run Storybook a11y tests" + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + - run: yarn install --immutable --check-cache + - name: Install Playwright browsers + run: npx playwright install --with-deps + - name: Start Storybook + run: yarn storybook & + - name: Run tests + # the chromium browser used by Playwright sets its locale to "en_US@posix" by default + # this is not a valid language code, and causes some stories to fail to load! + # instead, we set the LANG environment variable to en_US to override this + # see https://github.com/microsoft/playwright/issues/34046 + env: + LANG: en_US + run: npx wait-on --timeout 120000 http://localhost:9001 && yarn test:storybook diff --git a/package.json b/package.json index 36ebdd2b37b..b2518a6804e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "e2e:plugin:build:dev": "nx run-many -t dev --projects='@test-plugins/*' --maxParallel=100", "test": "jest --notify --watch", "test:coverage": "jest --coverage", + "test:storybook": "yarn workspace @grafana/ui storybook:test", "test:coverage:changes": "jest --coverage --changedSince=origin/main", "test:accessibility-report": "./scripts/generate-a11y-report.sh", "test:ci": "mkdir -p reports/junit && JEST_JUNIT_OUTPUT_DIR=reports/junit jest --ci --reporters=default --reporters=jest-junit -w ${TEST_MAX_WORKERS:-100%} --shard=${TEST_SHARD:-1}/${TEST_SHARD_TOTAL:-1}", diff --git a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx index 11033d82739..46f3f72ed6f 100644 --- a/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx +++ b/packages/grafana-alerting/src/grafana/contactPoints/components/ContactPointSelector/ContactPointSelector.story.tsx @@ -14,6 +14,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/.storybook/copyAssets.ts b/packages/grafana-ui/.storybook/copyAssets.ts index e49cd32b5d0..198953b30e3 100644 --- a/packages/grafana-ui/.storybook/copyAssets.ts +++ b/packages/grafana-ui/.storybook/copyAssets.ts @@ -4,7 +4,7 @@ // when publishing new storybook. // Note: Storybook has a static copying feature but it copies entire directories which can contain thousands of icons. -import { copySync, emptyDirSync, lstatSync } from 'fs-extra'; +import { existsSync, copySync, lstatSync } from 'fs-extra'; import { resolve } from 'node:path'; // avoid importing from @grafana/data to prevent error: window is not defined @@ -52,16 +52,13 @@ export function copyAssetsSync() { }, ]; - const staticDir = resolve(__dirname, 'static', 'public'); - - emptyDirSync(staticDir); - for (const asset of assets) { const fromPath = resolve(__dirname, asset.from); const toPath = resolve(__dirname, asset.to); - - copySync(fromPath, toPath, { - filter: (src) => !lstatSync(src).isSymbolicLink(), - }); + if (!existsSync(toPath)) { + copySync(fromPath, toPath, { + filter: (src) => !lstatSync(src).isSymbolicLink(), + }); + } } } diff --git a/packages/grafana-ui/.storybook/preview.ts b/packages/grafana-ui/.storybook/preview.ts index 803d45315e3..39542016eab 100644 --- a/packages/grafana-ui/.storybook/preview.ts +++ b/packages/grafana-ui/.storybook/preview.ts @@ -64,6 +64,7 @@ const preview: Preview = { docs: { container: ThemedDocsContainer, }, + a11y: { test: 'error' }, knobs: { disable: true, }, diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ec017b3e688..cdafde28217 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -53,6 +53,7 @@ "clean": "rimraf ./dist ./compiled ./unstable ./package.tgz", "storybook": "storybook dev -p 9001 -c .storybook --no-open", "storybook:build": "storybook build -o ./dist/storybook -c .storybook", + "storybook:test": "test-storybook --url http://localhost:9001", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && ALIAS_PACKAGE_NAME=unstable node ../../scripts/prepare-npm-package.js", "postpack": "mv package.json.bak package.json && rimraf ./unstable" @@ -152,6 +153,7 @@ "@storybook/preview-api": "^8.6.2", "@storybook/react": "^8.6.2", "@storybook/react-webpack5": "^8.6.2", + "@storybook/test-runner": "^0.23.0", "@storybook/theming": "^8.6.2", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", diff --git a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx index eb6cb427eba..24df1318d55 100644 --- a/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx +++ b/packages/grafana-ui/src/components/AutoSaveField/AutoSaveField.story.tsx @@ -35,6 +35,8 @@ const meta: Meta = { 'validationMessageHorizontalOverflow', ], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { saveErrorMessage: { control: 'text' }, diff --git a/packages/grafana-ui/src/components/Button/Button.story.tsx b/packages/grafana-ui/src/components/Button/Button.story.tsx index 5f26875852e..91746e38dc1 100644 --- a/packages/grafana-ui/src/components/Button/Button.story.tsx +++ b/packages/grafana-ui/src/components/Button/Button.story.tsx @@ -18,6 +18,8 @@ export default { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { size: { diff --git a/packages/grafana-ui/src/components/Carousel/Carousel.story.tsx b/packages/grafana-ui/src/components/Carousel/Carousel.story.tsx index 33a2c762582..68385877df8 100644 --- a/packages/grafana-ui/src/components/Carousel/Carousel.story.tsx +++ b/packages/grafana-ui/src/components/Carousel/Carousel.story.tsx @@ -19,6 +19,8 @@ const meta: Meta = { component: Carousel, parameters: { docs: { page: mdx }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { images: { diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx index 761235d5a22..789b4d04211 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.story.tsx @@ -49,6 +49,8 @@ const meta: Meta = { 'formatCreateLabel', ], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { onSelect, diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx index c3967136503..5b3f6921084 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx @@ -18,6 +18,8 @@ const meta: Meta = { controls: { exclude: ['onChange', 'onColorChange'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { enableNamedColors: false, diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx index 0be0684ca33..b0f637b7d7d 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.story.tsx @@ -11,6 +11,8 @@ const meta: Meta = { controls: { exclude: ['onChange', 'onColorChange'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx index 9456242aa8f..fd015178845 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.story.tsx @@ -17,6 +17,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx index 106dfc19af6..371e431185b 100644 --- a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx @@ -21,6 +21,8 @@ const meta: Meta = { controls: { exclude: ['className', 'onClick', 'onCancel', 'onConfirm'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { buttonText: 'Edit', diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx index ebafcf95e03..78587ebb1dd 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx @@ -19,6 +19,8 @@ const meta: Meta = { controls: { exclude: ['renderMenuItems', 'renderHeader', 'onClose', 'children'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { x: 200, diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx index d3678e8dc84..baea8271f34 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeOfDayPicker.story.tsx @@ -13,6 +13,8 @@ const meta: Meta = { controls: { exclude: ['onChange'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { value: dateTime(Date.now()), diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx index ac9b70eb14c..9ac5e775590 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.story.tsx @@ -10,6 +10,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/FormField/FormField.story.tsx b/packages/grafana-ui/src/components/FormField/FormField.story.tsx index 845001dcacc..9d39b20536b 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.story.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.story.tsx @@ -9,6 +9,8 @@ const meta: Meta = { controls: { exclude: ['inputEl'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { inputWidth: 20, diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx index fa4d1fb1d8e..f79388911bd 100644 --- a/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Checkbox.story.tsx @@ -18,6 +18,8 @@ const meta: Meta = { controls: { exclude: ['value', 'htmlValue'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Forms/Field.story.tsx b/packages/grafana-ui/src/components/Forms/Field.story.tsx index b3accd1e492..e4ed14c79d6 100644 --- a/packages/grafana-ui/src/components/Forms/Field.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Field.story.tsx @@ -26,6 +26,8 @@ const meta: Meta = { controls: { exclude: ['children', 'className'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Forms/FieldArray.story.tsx b/packages/grafana-ui/src/components/Forms/FieldArray.story.tsx index fa38cf6521e..69179f5c0a2 100644 --- a/packages/grafana-ui/src/components/Forms/FieldArray.story.tsx +++ b/packages/grafana-ui/src/components/Forms/FieldArray.story.tsx @@ -21,6 +21,8 @@ const meta: Meta = { controls: { exclude: ['name', 'keyName', 'control', 'shouldUnregister'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { containerWidth: { control: { type: 'range', min: 100, max: 500, step: 10 } }, diff --git a/packages/grafana-ui/src/components/Forms/FieldSet.story.tsx b/packages/grafana-ui/src/components/Forms/FieldSet.story.tsx index 96e66a07290..6d9037aa852 100644 --- a/packages/grafana-ui/src/components/Forms/FieldSet.story.tsx +++ b/packages/grafana-ui/src/components/Forms/FieldSet.story.tsx @@ -21,6 +21,8 @@ const meta: Meta = { controls: { exclude: ['children'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { label: { control: 'text' }, diff --git a/packages/grafana-ui/src/components/Forms/Form.story.tsx b/packages/grafana-ui/src/components/Forms/Form.story.tsx index 7c12beb7465..1ef5d4de60f 100644 --- a/packages/grafana-ui/src/components/Forms/Form.story.tsx +++ b/packages/grafana-ui/src/components/Forms/Form.story.tsx @@ -23,6 +23,8 @@ export default { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Forms/InlineField.story.tsx b/packages/grafana-ui/src/components/Forms/InlineField.story.tsx index da42f012f3a..9ecff0b22e7 100644 --- a/packages/grafana-ui/src/components/Forms/InlineField.story.tsx +++ b/packages/grafana-ui/src/components/Forms/InlineField.story.tsx @@ -23,6 +23,8 @@ const meta: Meta = { controls: { exclude: ['htmlFor', 'className', 'children'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.story.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.story.tsx index 4fe04474b16..8f8dadc33f0 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.story.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.story.tsx @@ -14,6 +14,8 @@ const meta: Meta = { controls: { exclude: ['className', 'options', 'value', 'onChange', 'onClick', 'id'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { disabledOptions: { diff --git a/packages/grafana-ui/src/components/InlineToast/InlineToast.story.tsx b/packages/grafana-ui/src/components/InlineToast/InlineToast.story.tsx index 3173eb4751d..a232b737aed 100644 --- a/packages/grafana-ui/src/components/InlineToast/InlineToast.story.tsx +++ b/packages/grafana-ui/src/components/InlineToast/InlineToast.story.tsx @@ -14,6 +14,8 @@ const story: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { referenceElement: { diff --git a/packages/grafana-ui/src/components/Input/Input.story.tsx b/packages/grafana-ui/src/components/Input/Input.story.tsx index eab7f1d3ddc..20da7ce5aac 100644 --- a/packages/grafana-ui/src/components/Input/Input.story.tsx +++ b/packages/grafana-ui/src/components/Input/Input.story.tsx @@ -31,6 +31,8 @@ const meta: Meta = { controls: { exclude: ['prefix', 'suffix', 'addonBefore', 'addonAfter'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { type: 'text', diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx index e0df9d9782f..c8cf16cf2b6 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.story.tsx @@ -112,6 +112,8 @@ const meta: Meta> = { controls: { exclude: EXCLUDED_PROPS, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { columns: [ diff --git a/packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx b/packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx index eee4984019c..f7ec6fcfbf5 100644 --- a/packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Grid/Grid.story.tsx @@ -17,6 +17,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { gap: 1, diff --git a/packages/grafana-ui/src/components/Layout/Stack/Stack.story.tsx b/packages/grafana-ui/src/components/Layout/Stack/Stack.story.tsx index 5acad6ab10f..14e4e3b5124 100644 --- a/packages/grafana-ui/src/components/Layout/Stack/Stack.story.tsx +++ b/packages/grafana-ui/src/components/Layout/Stack/Stack.story.tsx @@ -16,6 +16,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Link/TextLink.story.tsx b/packages/grafana-ui/src/components/Link/TextLink.story.tsx index 6c1b9eb5fff..b353b15674f 100644 --- a/packages/grafana-ui/src/components/Link/TextLink.story.tsx +++ b/packages/grafana-ui/src/components/Link/TextLink.story.tsx @@ -15,6 +15,8 @@ const meta: Meta = { page: mdx, }, controls: { exclude: ['href', 'external'] }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { variant: { diff --git a/packages/grafana-ui/src/components/LoadingBar/LoadingBar.story.tsx b/packages/grafana-ui/src/components/LoadingBar/LoadingBar.story.tsx index 3b6e412cb59..1d17511c54d 100644 --- a/packages/grafana-ui/src/components/LoadingBar/LoadingBar.story.tsx +++ b/packages/grafana-ui/src/components/LoadingBar/LoadingBar.story.tsx @@ -17,6 +17,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Menu/Menu.story.tsx b/packages/grafana-ui/src/components/Menu/Menu.story.tsx index d0ea0726b3a..131486ce175 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.story.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.story.tsx @@ -24,6 +24,8 @@ const meta: Meta = { actions: { disabled: true, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Modal/Modal.story.tsx b/packages/grafana-ui/src/components/Modal/Modal.story.tsx index 92e8e13348c..df05238cb3a 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.story.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.story.tsx @@ -21,6 +21,8 @@ const meta: Meta = { controls: { exclude: ['className', 'contentClassName', 'onDismiss', 'onClickBackdrop'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { body: oneLineTrim(`Id incididunt do pariatur qui labore. Sint culpa irure cillum et ullamco proident. Deserunt ipsum velit dolore est enim proident dolore consectetur. Et cillum tempor pariatur et. Est tempor cillum ad id nulla. Cillum ut proident diff --git a/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx b/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx index 4a308a6d0be..d7a85381e9e 100644 --- a/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx +++ b/packages/grafana-ui/src/components/PageLayout/PageToolbar.story.tsx @@ -11,7 +11,10 @@ import { PageToolbar } from './PageToolbar'; const meta: Meta = { title: 'Navigation/Deprecated/PageToolbar', component: PageToolbar, - parameters: {}, + parameters: { + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, + }, }; export const Examples = () => { diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx index e401f94a162..afefc6b7968 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.story.tsx @@ -31,6 +31,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx index 4b11c87e4ac..f0f672fa120 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx @@ -24,6 +24,8 @@ const meta: Meta = { 'syntaxLoaded', ], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { query: { diff --git a/packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.story.tsx b/packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.story.tsx index 2d3cf51788a..a6dfbf973eb 100644 --- a/packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.story.tsx +++ b/packages/grafana-ui/src/components/ScrollContainer/ScrollContainer.story.tsx @@ -14,6 +14,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: {}, args: { diff --git a/packages/grafana-ui/src/components/Segment/Segment.story.tsx b/packages/grafana-ui/src/components/Segment/Segment.story.tsx index 91ae6c47bfb..982776c5415 100644 --- a/packages/grafana-ui/src/components/Segment/Segment.story.tsx +++ b/packages/grafana-ui/src/components/Segment/Segment.story.tsx @@ -62,6 +62,10 @@ export const ArrayOptions = () => { const meta: Meta = { title: 'Inputs/Segment', component: Segment, + parameters: { + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, + }, }; export const ArrayOptionsWithPrimitiveValue = () => { diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx index 9b64c2fca74..fa4b97b477b 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.story.tsx @@ -63,6 +63,10 @@ export const ArrayOptions = () => { const meta: Meta = { title: 'Inputs/SegmentAsync', component: SegmentAsync, + parameters: { + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, + }, }; export const ArrayOptionsWithPrimitiveValue = () => { diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx index 780f6b4aba2..74af7d58fda 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentInput.story.tsx @@ -32,6 +32,10 @@ export const BasicInput = () => { const meta: Meta = { title: 'Inputs/SegmentInput', component: SegmentInput, + parameters: { + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, + }, }; export const BasicInputWithPlaceholder = () => { diff --git a/packages/grafana-ui/src/components/Select/Select.story.tsx b/packages/grafana-ui/src/components/Select/Select.story.tsx index 7e8388442a1..bf3a92de89a 100644 --- a/packages/grafana-ui/src/components/Select/Select.story.tsx +++ b/packages/grafana-ui/src/components/Select/Select.story.tsx @@ -72,6 +72,8 @@ const meta: Meta = { 'value', ], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { width: 0, diff --git a/packages/grafana-ui/src/components/Select/SelectPerf.story.tsx b/packages/grafana-ui/src/components/Select/SelectPerf.story.tsx index 357eee70561..8a5ff8f9433 100644 --- a/packages/grafana-ui/src/components/Select/SelectPerf.story.tsx +++ b/packages/grafana-ui/src/components/Select/SelectPerf.story.tsx @@ -16,6 +16,10 @@ const meta: Meta = { control: { type: 'number' }, }, }, + parameters: { + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, + }, }; export default meta; diff --git a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx index eccb916483a..d00648bdfbe 100644 --- a/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/RangeSlider.story.tsx @@ -9,6 +9,8 @@ const meta: Meta = { controls: { exclude: ['tooltipAlwaysVisible'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { orientation: { control: { type: 'select', options: ['horizontal', 'vertical'] } }, diff --git a/packages/grafana-ui/src/components/Slider/Slider.story.tsx b/packages/grafana-ui/src/components/Slider/Slider.story.tsx index a4180712444..770038dbc64 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.story.tsx @@ -12,6 +12,8 @@ const meta: Meta = { knobs: { disabled: true, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { orientation: { control: { type: 'select', options: ['horizontal', 'vertical'] } }, diff --git a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx index 7e20cb9288f..3487a13693f 100644 --- a/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx +++ b/packages/grafana-ui/src/components/StatsPicker/StatsPicker.story.tsx @@ -43,6 +43,8 @@ const meta: Meta = { controls: { exclude: ['onChange', 'stats', 'defaultStat', 'className'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/Switch/Switch.story.tsx b/packages/grafana-ui/src/components/Switch/Switch.story.tsx index ae4786d0fe5..67a9567a2d5 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.story.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.story.tsx @@ -16,6 +16,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { disabled: false, diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 35944868d22..b6c97df317e 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -32,6 +32,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { width: 700, diff --git a/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx index 124dfc46af9..494d0e20eee 100644 --- a/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx +++ b/packages/grafana-ui/src/components/TableInputCSV/TableInputCSV.story.tsx @@ -8,6 +8,10 @@ import { TableInputCSV } from './TableInputCSV'; const meta: Meta = { title: 'Inputs/Deprecated/TableInputCSV', component: TableInputCSV, + parameters: { + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, + }, }; export const basic: StoryFn = () => { diff --git a/packages/grafana-ui/src/components/Tags/TagList.story.tsx b/packages/grafana-ui/src/components/Tags/TagList.story.tsx index 7d8c6094de3..e6f16c07028 100644 --- a/packages/grafana-ui/src/components/Tags/TagList.story.tsx +++ b/packages/grafana-ui/src/components/Tags/TagList.story.tsx @@ -14,6 +14,8 @@ const meta: Meta = { controls: { exclude: ['className', 'onClick', 'getAriaLabel'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { displayMax: 3, diff --git a/packages/grafana-ui/src/components/Text/Text.story.tsx b/packages/grafana-ui/src/components/Text/Text.story.tsx index f23056df839..359b7f7853b 100644 --- a/packages/grafana-ui/src/components/Text/Text.story.tsx +++ b/packages/grafana-ui/src/components/Text/Text.story.tsx @@ -13,6 +13,8 @@ const meta: Meta = { docs: { page: mdx, }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { variant: { diff --git a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx index 32fa7f40835..682b9f95a88 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/ThemeDemo.story.tsx @@ -11,6 +11,8 @@ const meta: Meta = { showPanel: false, }, docs: {}, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, }; diff --git a/packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx b/packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx index 0bc75786461..148d2f9002d 100644 --- a/packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx +++ b/packages/grafana-ui/src/components/ThemeDemos/Typography.story.tsx @@ -9,6 +9,10 @@ import { Text } from '../Text/Text'; const meta: Meta = { title: 'Developers/Typography', + parameters: { + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, + }, }; const FONT_WEIGHTS = [/*100, 200, 300, */ 400, 500 /*600, 700, 800, 900*/]; diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx index 04c66996a26..e200c381deb 100644 --- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx +++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.story.tsx @@ -18,6 +18,8 @@ const meta: Meta = { controls: { exclude: ['imgSrc', 'imgAlt', 'narrow'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, args: { variant: 'default', diff --git a/packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx b/packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx index 939329c821f..d73c22c82c4 100644 --- a/packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx +++ b/packages/grafana-ui/src/components/VizLayout/VizLayout.story.tsx @@ -11,6 +11,8 @@ const meta: Meta = { controls: { exclude: ['legend'], }, + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, }, argTypes: { width: { control: { type: 'range', min: 100, max: 1000 } }, diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx index b9af4d23fc4..cba6f984501 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegend.story.tsx @@ -16,6 +16,10 @@ const meta: Meta = { containerWidth: '100%', seriesCount: 5, }, + parameters: { + // TODO fix a11y issue in story and remove this + a11y: { test: 'off' }, + }, argTypes: { containerWidth: { control: { diff --git a/yarn.lock b/yarn.lock index ef2b85d416f..e0dc68b1692 100644 --- a/yarn.lock +++ b/yarn.lock @@ -139,7 +139,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.28.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.22.9": +"@babel/core@npm:7.28.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.22.5, @babel/core@npm:^7.22.9, @babel/core@npm:^7.7.5": version: 7.28.0 resolution: "@babel/core@npm:7.28.0" dependencies: @@ -162,7 +162,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.22.9, @babel/generator@npm:^7.28.0, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.22.5, @babel/generator@npm:^7.22.9, @babel/generator@npm:^7.28.0, @babel/generator@npm:^7.7.2": version: 7.28.0 resolution: "@babel/generator@npm:7.28.0" dependencies: @@ -3775,6 +3775,7 @@ __metadata: "@storybook/preview-api": "npm:^8.6.2" "@storybook/react": "npm:^8.6.2" "@storybook/react-webpack5": "npm:^8.6.2" + "@storybook/test-runner": "npm:^0.23.0" "@storybook/theming": "npm:^8.6.2" "@tanstack/react-virtual": "npm:^3.5.1" "@testing-library/dom": "npm:10.4.0" @@ -3894,6 +3895,22 @@ __metadata: languageName: node linkType: hard +"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": + version: 9.3.0 + resolution: "@hapi/hoek@npm:9.3.0" + checksum: 10/ad83a223787749f3873bce42bd32a9a19673765bf3edece0a427e138859ff729469e68d5fdf9ff6bbee6fb0c8e21bab61415afa4584f527cfc40b59ea1957e70 + languageName: node + linkType: hard + +"@hapi/topo@npm:^5.1.0": + version: 5.1.0 + resolution: "@hapi/topo@npm:5.1.0" + dependencies: + "@hapi/hoek": "npm:^9.0.0" + checksum: 10/084bfa647015f4fd3fdd51fadb2747d09ef2f5e1443d6cbada2988b0c88494f85edf257ec606c790db146ac4e34ff57f3fcb22e3299b8e06ed5c87ba7583495c + languageName: node + linkType: hard + "@hello-pangea/dnd@npm:18.0.1": version: 18.0.1 resolution: "@hello-pangea/dnd@npm:18.0.1" @@ -7194,6 +7211,29 @@ __metadata: languageName: node linkType: hard +"@sideway/address@npm:^4.1.5": + version: 4.1.5 + resolution: "@sideway/address@npm:4.1.5" + dependencies: + "@hapi/hoek": "npm:^9.0.0" + checksum: 10/c4c73ac0339504f34e016d3a687118e7ddf197c1c968579572123b67b230be84caa705f0f634efdfdde7f2e07a6e0224b3c70665dc420d8bc95bf400cfc4c998 + languageName: node + linkType: hard + +"@sideway/formula@npm:^3.0.1": + version: 3.0.1 + resolution: "@sideway/formula@npm:3.0.1" + checksum: 10/8d3ee7f80df4e5204b2cbe92a2a711ca89684965a5c9eb3b316b7051212d3522e332a65a0bb2a07cc708fcd1d0b27fcb30f43ff0bcd5089d7006c7160a89eefe + languageName: node + linkType: hard + +"@sideway/pinpoint@npm:^2.0.0": + version: 2.0.0 + resolution: "@sideway/pinpoint@npm:2.0.0" + checksum: 10/1ed21800128b2b23280ba4c9db26c8ff6142b97a8683f17639fd7f2128aa09046461574800b30fb407afc5b663c2331795ccf3b654d4b38fa096e41a5c786bf8 + languageName: node + linkType: hard + "@sigstore/bundle@npm:^2.3.2": version: 2.3.2 resolution: "@sigstore/bundle@npm:2.3.2" @@ -7785,6 +7825,36 @@ __metadata: languageName: node linkType: hard +"@storybook/test-runner@npm:^0.23.0": + version: 0.23.0 + resolution: "@storybook/test-runner@npm:0.23.0" + dependencies: + "@babel/core": "npm:^7.22.5" + "@babel/generator": "npm:^7.22.5" + "@babel/template": "npm:^7.22.5" + "@babel/types": "npm:^7.22.5" + "@jest/types": "npm:^29.6.3" + "@swc/core": "npm:^1.5.22" + "@swc/jest": "npm:^0.2.23" + expect-playwright: "npm:^0.8.0" + jest: "npm:^29.6.4" + jest-circus: "npm:^29.6.4" + jest-environment-node: "npm:^29.6.4" + jest-junit: "npm:^16.0.0" + jest-playwright-preset: "npm:^4.0.0" + jest-runner: "npm:^29.6.4" + jest-serializer-html: "npm:^7.1.0" + jest-watch-typeahead: "npm:^2.0.0" + nyc: "npm:^15.1.0" + playwright: "npm:^1.14.0" + peerDependencies: + storybook: ^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 + bin: + test-storybook: dist/test-storybook.js + checksum: 10/42cb993ea3647cbe71495ea095c71d28970b465796dd6b85c15bf41d7fbc3d0f44b0eceb55b200d34d70d2e05e681a759d7164c46f911a43a338ec918d188e25 + languageName: node + linkType: hard + "@storybook/test@npm:8.6.2": version: 8.6.2 resolution: "@storybook/test@npm:8.6.2" @@ -8397,6 +8467,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-arm64@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-darwin-arm64@npm:1.13.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-darwin-x64@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-darwin-x64@npm:1.10.12" @@ -8411,6 +8488,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-darwin-x64@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-darwin-x64@npm:1.13.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@swc/core-linux-arm-gnueabihf@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.12" @@ -8425,6 +8509,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm-gnueabihf@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@swc/core-linux-arm64-gnu@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-linux-arm64-gnu@npm:1.10.12" @@ -8439,6 +8530,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-gnu@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-linux-arm64-gnu@npm:1.13.2" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-arm64-musl@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-linux-arm64-musl@npm:1.10.12" @@ -8453,6 +8551,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-arm64-musl@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-linux-arm64-musl@npm:1.13.2" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@swc/core-linux-x64-gnu@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-linux-x64-gnu@npm:1.10.12" @@ -8467,6 +8572,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-gnu@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-linux-x64-gnu@npm:1.13.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@swc/core-linux-x64-musl@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-linux-x64-musl@npm:1.10.12" @@ -8481,6 +8593,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-linux-x64-musl@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-linux-x64-musl@npm:1.13.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@swc/core-win32-arm64-msvc@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-win32-arm64-msvc@npm:1.10.12" @@ -8495,6 +8614,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-arm64-msvc@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-win32-arm64-msvc@npm:1.13.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@swc/core-win32-ia32-msvc@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-win32-ia32-msvc@npm:1.10.12" @@ -8509,6 +8635,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-ia32-msvc@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-win32-ia32-msvc@npm:1.13.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@swc/core-win32-x64-msvc@npm:1.10.12": version: 1.10.12 resolution: "@swc/core-win32-x64-msvc@npm:1.10.12" @@ -8523,6 +8656,13 @@ __metadata: languageName: node linkType: hard +"@swc/core-win32-x64-msvc@npm:1.13.2": + version: 1.13.2 + resolution: "@swc/core-win32-x64-msvc@npm:1.13.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@swc/core@npm:1.10.12": version: 1.10.12 resolution: "@swc/core@npm:1.10.12" @@ -8615,6 +8755,52 @@ __metadata: languageName: node linkType: hard +"@swc/core@npm:^1.5.22": + version: 1.13.2 + resolution: "@swc/core@npm:1.13.2" + dependencies: + "@swc/core-darwin-arm64": "npm:1.13.2" + "@swc/core-darwin-x64": "npm:1.13.2" + "@swc/core-linux-arm-gnueabihf": "npm:1.13.2" + "@swc/core-linux-arm64-gnu": "npm:1.13.2" + "@swc/core-linux-arm64-musl": "npm:1.13.2" + "@swc/core-linux-x64-gnu": "npm:1.13.2" + "@swc/core-linux-x64-musl": "npm:1.13.2" + "@swc/core-win32-arm64-msvc": "npm:1.13.2" + "@swc/core-win32-ia32-msvc": "npm:1.13.2" + "@swc/core-win32-x64-msvc": "npm:1.13.2" + "@swc/counter": "npm:^0.1.3" + "@swc/types": "npm:^0.1.23" + peerDependencies: + "@swc/helpers": ">=0.5.17" + dependenciesMeta: + "@swc/core-darwin-arm64": + optional: true + "@swc/core-darwin-x64": + optional: true + "@swc/core-linux-arm-gnueabihf": + optional: true + "@swc/core-linux-arm64-gnu": + optional: true + "@swc/core-linux-arm64-musl": + optional: true + "@swc/core-linux-x64-gnu": + optional: true + "@swc/core-linux-x64-musl": + optional: true + "@swc/core-win32-arm64-msvc": + optional: true + "@swc/core-win32-ia32-msvc": + optional: true + "@swc/core-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@swc/helpers": + optional: true + checksum: 10/ca4d05c44c3e36f7d3f68dda9b53eb1e7e1c9f07701d526c8484605bf4a744cbcb83e1cadb0b4b57723d98ba799d3a75508b2219751ad5fdb29491ca325c680d + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -8631,7 +8817,7 @@ __metadata: languageName: node linkType: hard -"@swc/jest@npm:^0.2.26": +"@swc/jest@npm:^0.2.23, @swc/jest@npm:^0.2.26": version: 0.2.39 resolution: "@swc/jest@npm:0.2.39" dependencies: @@ -8653,6 +8839,15 @@ __metadata: languageName: node linkType: hard +"@swc/types@npm:^0.1.23": + version: 0.1.23 + resolution: "@swc/types@npm:0.1.23" + dependencies: + "@swc/counter": "npm:^0.1.3" + checksum: 10/8d9d73dd1fc9335105105da57595ab913bad6addd4fbcb2eb147300694630232225eb7dc74b733205af33352803e4fcefc18e3a36f8924cf821ef91384767670 + languageName: node + linkType: hard + "@tanstack/react-virtual@npm:^3.5.1, @tanstack/react-virtual@npm:^3.9.0": version: 3.10.9 resolution: "@tanstack/react-virtual@npm:3.10.9" @@ -10197,6 +10392,15 @@ __metadata: languageName: node linkType: hard +"@types/wait-on@npm:^5.2.0": + version: 5.3.4 + resolution: "@types/wait-on@npm:5.3.4" + dependencies: + "@types/node": "npm:*" + checksum: 10/2711a9ef4af3995efd442acb806d3bba0a03ef3a7eb07b1734ddd8a42b6682414f6b57ed7536fb6a5df6c6449ca24c3d42924a9d896ae3eb1065b2abbe988a18 + languageName: node + linkType: hard + "@types/webpack-assets-manifest@npm:^5": version: 5.1.4 resolution: "@types/webpack-assets-manifest@npm:5.1.4" @@ -11297,6 +11501,15 @@ __metadata: languageName: node linkType: hard +"append-transform@npm:^2.0.0": + version: 2.0.0 + resolution: "append-transform@npm:2.0.0" + dependencies: + default-require-extensions: "npm:^3.0.0" + checksum: 10/f26f393bf7a428fd1bb18f2758a819830a582243310c5170edb3f98fdc5a535333d02b952f7c2d9b14522bd8ead5b132a0b15000eca18fa9f49172963ebbc231 + languageName: node + linkType: hard + "aproba@npm:2.0.0": version: 2.0.0 resolution: "aproba@npm:2.0.0" @@ -11318,6 +11531,13 @@ __metadata: languageName: node linkType: hard +"archy@npm:^1.0.0": + version: 1.0.0 + resolution: "archy@npm:1.0.0" + checksum: 10/d7928049a57988b86df3f4de75ca16a4252ccee591d085c627e649fc54c5ae5daa833f17aa656bd825bd00bc0a2756ae03d2b983050bdbda1046b6d832bf7303 + languageName: node + linkType: hard + "are-docs-informative@npm:^0.0.2": version: 0.0.2 resolution: "are-docs-informative@npm:0.0.2" @@ -11405,7 +11625,7 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9": +"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": version: 3.1.9 resolution: "array-includes@npm:3.1.9" dependencies: @@ -11465,22 +11685,21 @@ __metadata: languageName: node linkType: hard -"array.prototype.findlastindex@npm:^1.2.6": - version: 1.2.6 - resolution: "array.prototype.findlastindex@npm:1.2.6" +"array.prototype.findlastindex@npm:^1.2.5": + version: 1.2.5 + resolution: "array.prototype.findlastindex@npm:1.2.5" dependencies: - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.4" + call-bind: "npm:^1.0.7" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.9" + es-abstract: "npm:^1.23.2" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.1.1" - es-shim-unscopables: "npm:^1.1.0" - checksum: 10/5ddb6420e820bef6ddfdcc08ce780d0fd5e627e97457919c27e32359916de5a11ce12f7c55073555e503856618eaaa70845d6ca11dcba724766f38eb1c22f7a2 + es-object-atoms: "npm:^1.0.0" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10/7c5c821f357cd53ab6cc305de8086430dd8d7a2485db87b13f843e868055e9582b1fd338f02338f67fc3a1603ceaf9610dd2a470b0b506f9d18934780f95b246 languageName: node linkType: hard -"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.3": +"array.prototype.flat@npm:^1.3.1": version: 1.3.3 resolution: "array.prototype.flat@npm:1.3.3" dependencies: @@ -11492,6 +11711,18 @@ __metadata: languageName: node linkType: hard +"array.prototype.flat@npm:^1.3.2": + version: 1.3.2 + resolution: "array.prototype.flat@npm:1.3.2" + dependencies: + call-bind: "npm:^1.0.2" + define-properties: "npm:^1.2.0" + es-abstract: "npm:^1.22.1" + es-shim-unscopables: "npm:^1.0.0" + checksum: 10/d9d2f6f27584de92ec7995bc931103e6de722cd2498bdbfc4cba814fc3e52f056050a93be883018811f7c0a35875f5056584a0e940603a5e5934f0279896aebe + languageName: node + linkType: hard + "array.prototype.flatmap@npm:^1.3.2, array.prototype.flatmap@npm:^1.3.3": version: 1.3.3 resolution: "array.prototype.flatmap@npm:1.3.3" @@ -11741,6 +11972,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.6.1": + version: 1.11.0 + resolution: "axios@npm:1.11.0" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10/232df4af7a4e4e07baa84621b9cc4b0c518a757b4eacc7f635c0eb3642cb98dff347326739f24b891b3b4481b7b838c79a3a0c4819c9fbc1fc40232431b9c5dc + languageName: node + linkType: hard + "axobject-query@npm:^4.1.0": version: 4.1.0 resolution: "axobject-query@npm:4.1.0" @@ -12369,6 +12611,18 @@ __metadata: languageName: node linkType: hard +"caching-transform@npm:^4.0.0": + version: 4.0.0 + resolution: "caching-transform@npm:4.0.0" + dependencies: + hasha: "npm:^5.0.0" + make-dir: "npm:^3.0.0" + package-hash: "npm:^4.0.0" + write-file-atomic: "npm:^3.0.0" + checksum: 10/7e7ca628511ab18c86eea1231834d2591de29a13ae771a7d9ab85be8c6e53e45c5a5b0d0d95d4a3274fc4f26c16956a98162e40c191c131204b5d5aa949660b5 + languageName: node + linkType: hard + "calculate-size@npm:1.1.1": version: 1.1.1 resolution: "calculate-size@npm:1.1.1" @@ -12450,7 +12704,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.3.1": +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: 10/e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b @@ -12569,7 +12823,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": +"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2, chalk@npm:~4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -12924,6 +13178,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^6.2.0" + checksum: 10/44afbcc29df0899e87595590792a871cd8c4bc7d6ce92832d9ae268d141a77022adafca1aeaeccff618b62a613b8354e57fe22a275c199ec04baf00d381ef6ab + languageName: node + linkType: hard + "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -13183,6 +13448,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^3.0.2": + version: 3.0.2 + resolution: "commander@npm:3.0.2" + checksum: 10/f42053569f5954498246783465b39139917a51284bf3361574c9f731fea27a4bd6452dbb1755cc2d923c7b47dfea67930037c7b7e862288f2c397cec9a74da87 + languageName: node + linkType: hard + "commander@npm:^6.2.0, commander@npm:^6.2.1, commander@npm:~6.2.1": version: 6.2.1 resolution: "commander@npm:6.2.1" @@ -13451,6 +13723,13 @@ __metadata: languageName: node linkType: hard +"convert-source-map@npm:^1.7.0": + version: 1.9.0 + resolution: "convert-source-map@npm:1.9.0" + checksum: 10/dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 + languageName: node + linkType: hard + "convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" @@ -14046,6 +14325,16 @@ __metadata: languageName: node linkType: hard +"cwd@npm:^0.10.0": + version: 0.10.0 + resolution: "cwd@npm:0.10.0" + dependencies: + find-pkg: "npm:^0.1.2" + fs-exists-sync: "npm:^0.1.0" + checksum: 10/fdb2d1e8e1317e488bf0b7e569a17f929a45c4f2e706820c843a6be460cf8b334c43ac22c22c58c4f6258e501904ec1d1558d67c345002268fb40e3419f909da + languageName: node + linkType: hard + "cypress-file-upload@npm:5.0.8": version: 5.0.8 resolution: "cypress-file-upload@npm:5.0.8" @@ -14623,7 +14912,7 @@ __metadata: languageName: node linkType: hard -"decamelize@npm:^1.1.0": +"decamelize@npm:^1.1.0, decamelize@npm:^1.2.0": version: 1.2.0 resolution: "decamelize@npm:1.2.0" checksum: 10/ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa @@ -14707,6 +14996,15 @@ __metadata: languageName: node linkType: hard +"default-require-extensions@npm:^3.0.0": + version: 3.0.1 + resolution: "default-require-extensions@npm:3.0.1" + dependencies: + strip-bom: "npm:^4.0.0" + checksum: 10/45882fc971dd157faf6716ced04c15cf252c0a2d6f5c5844b66ca49f46ed03396a26cd940771aa569927aee22923a961bab789e74b25aabc94d90742c9dd1217 + languageName: node + linkType: hard + "defaults@npm:^1.0.3": version: 1.0.3 resolution: "defaults@npm:1.0.3" @@ -14734,7 +15032,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -14884,6 +15182,15 @@ __metadata: languageName: node linkType: hard +"diffable-html@npm:^4.1.0": + version: 4.1.0 + resolution: "diffable-html@npm:4.1.0" + dependencies: + htmlparser2: "npm:^3.9.2" + checksum: 10/3dd0f2a1e956be394a7da47caa9cb593073d91e3eb30d79de00664941f2d68a2549c7e392da1037afb45de1cf37b12e8b7dbaa312d47186061c66c9ce4aaa601 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -14971,6 +15278,16 @@ __metadata: languageName: node linkType: hard +"dom-serializer@npm:0": + version: 0.2.2 + resolution: "dom-serializer@npm:0.2.2" + dependencies: + domelementtype: "npm:^2.0.1" + entities: "npm:^2.0.0" + checksum: 10/376344893e4feccab649a14ca1a46473e9961f40fe62479ea692d4fee4d9df1c00ca8654811a79c1ca7b020096987e1ca4fb4d7f8bae32c1db800a680a0e5d5e + languageName: node + linkType: hard + "dom-serializer@npm:^1.0.1": version: 1.3.2 resolution: "dom-serializer@npm:1.3.2" @@ -14993,6 +15310,13 @@ __metadata: languageName: node linkType: hard +"domelementtype@npm:1, domelementtype@npm:^1.3.1": + version: 1.3.1 + resolution: "domelementtype@npm:1.3.1" + checksum: 10/7893da40218ae2106ec6ffc146b17f203487a52f5228b032ea7aa470e41dfe03e1bd762d0ee0139e792195efda765434b04b43cddcf63207b098f6ae44b36ad6 + languageName: node + linkType: hard + "domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0": version: 2.3.0 resolution: "domelementtype@npm:2.3.0" @@ -15009,6 +15333,15 @@ __metadata: languageName: node linkType: hard +"domhandler@npm:^2.3.0": + version: 2.4.2 + resolution: "domhandler@npm:2.4.2" + dependencies: + domelementtype: "npm:1" + checksum: 10/d8b0303c53c0eda912e45820ef8f6023f8462a724e8b824324f27923970222a250c7569e067de398c4d9ca3ce0f2b2d2818bc632d6fa72956721d6729479a9b9 + languageName: node + linkType: hard + "domhandler@npm:^4.0.0, domhandler@npm:^4.2.0": version: 4.2.2 resolution: "domhandler@npm:4.2.2" @@ -15039,6 +15372,16 @@ __metadata: languageName: node linkType: hard +"domutils@npm:^1.5.1": + version: 1.7.0 + resolution: "domutils@npm:1.7.0" + dependencies: + dom-serializer: "npm:0" + domelementtype: "npm:1" + checksum: 10/8c1d879fd3bbfc0156c970d12ebdf530f541cbda895d7f631b2444d22bbb9d0e5a3a4c3210cffb17708ad67531d7d40e1bef95e915c53a218d268607b66b63c8 + languageName: node + linkType: hard + "domutils@npm:^2.5.2, domutils@npm:^2.6.0": version: 2.8.0 resolution: "domutils@npm:2.8.0" @@ -15362,6 +15705,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^1.1.1": + version: 1.1.2 + resolution: "entities@npm:1.1.2" + checksum: 10/4a707022f4e932060f03df2526be55d085a2576fe534421e5b22bc62abb0d1f04241c171f9981e3d7baa4f4160606cad72a2f7eb01b6a25e279e3f31a2be4bf2 + languageName: node + linkType: hard + "entities@npm:^2.0.0": version: 2.2.0 resolution: "entities@npm:2.2.0" @@ -15520,6 +15870,65 @@ __metadata: languageName: node linkType: hard +"es-abstract@npm:^1.22.1": + version: 1.23.9 + resolution: "es-abstract@npm:1.23.9" + dependencies: + array-buffer-byte-length: "npm:^1.0.2" + arraybuffer.prototype.slice: "npm:^1.0.4" + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.3" + data-view-buffer: "npm:^1.0.2" + data-view-byte-length: "npm:^1.0.2" + data-view-byte-offset: "npm:^1.0.1" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + es-set-tostringtag: "npm:^2.1.0" + es-to-primitive: "npm:^1.3.0" + function.prototype.name: "npm:^1.1.8" + get-intrinsic: "npm:^1.2.7" + get-proto: "npm:^1.0.0" + get-symbol-description: "npm:^1.1.0" + globalthis: "npm:^1.0.4" + gopd: "npm:^1.2.0" + has-property-descriptors: "npm:^1.0.2" + has-proto: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + internal-slot: "npm:^1.1.0" + is-array-buffer: "npm:^3.0.5" + is-callable: "npm:^1.2.7" + is-data-view: "npm:^1.0.2" + is-regex: "npm:^1.2.1" + is-shared-array-buffer: "npm:^1.0.4" + is-string: "npm:^1.1.1" + is-typed-array: "npm:^1.1.15" + is-weakref: "npm:^1.1.0" + math-intrinsics: "npm:^1.1.0" + object-inspect: "npm:^1.13.3" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.7" + own-keys: "npm:^1.0.1" + regexp.prototype.flags: "npm:^1.5.3" + safe-array-concat: "npm:^1.1.3" + safe-push-apply: "npm:^1.0.0" + safe-regex-test: "npm:^1.1.0" + set-proto: "npm:^1.0.0" + string.prototype.trim: "npm:^1.2.10" + string.prototype.trimend: "npm:^1.0.9" + string.prototype.trimstart: "npm:^1.0.8" + typed-array-buffer: "npm:^1.0.3" + typed-array-byte-length: "npm:^1.0.3" + typed-array-byte-offset: "npm:^1.0.4" + typed-array-length: "npm:^1.0.7" + unbox-primitive: "npm:^1.1.0" + which-typed-array: "npm:^1.1.18" + checksum: 10/31a321966d760d88fc2ed984104841b42f4f24fc322b246002b9be0af162e03803ee41fcc3cf8be89e07a27ba3033168f877dd983703cb81422ffe5322a27582 + languageName: node + linkType: hard + "es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1": version: 1.0.1 resolution: "es-define-property@npm:1.0.1" @@ -15586,7 +15995,16 @@ __metadata: languageName: node linkType: hard -"es-shim-unscopables@npm:^1.0.2, es-shim-unscopables@npm:^1.1.0": +"es-shim-unscopables@npm:^1.0.0": + version: 1.0.2 + resolution: "es-shim-unscopables@npm:1.0.2" + dependencies: + hasown: "npm:^2.0.0" + checksum: 10/6d3bf91f658a27cc7217cd32b407a0d714393a84d125ad576319b9e83a893bea165cf41270c29e9ceaa56d3cf41608945d7e2a2c31fd51c0009b0c31402b91c7 + languageName: node + linkType: hard + +"es-shim-unscopables@npm:^1.0.2": version: 1.1.0 resolution: "es-shim-unscopables@npm:1.1.0" dependencies: @@ -15618,6 +16036,13 @@ __metadata: languageName: node linkType: hard +"es6-error@npm:^4.0.1": + version: 4.1.1 + resolution: "es6-error@npm:4.1.1" + checksum: 10/48483c25701dc5a6376f39bbe2eaf5da0b505607ec5a98cd3ade472c1939242156660636e2e508b33211e48e88b132d245341595c067bd4a95ac79fa7134da06 + languageName: node + linkType: hard + "es6-object-assign@npm:^1.1.0": version: 1.1.0 resolution: "es6-object-assign@npm:1.1.0" @@ -15841,44 +16266,44 @@ __metadata: languageName: node linkType: hard -"eslint-module-utils@npm:^2.12.1": - version: 2.12.1 - resolution: "eslint-module-utils@npm:2.12.1" +"eslint-module-utils@npm:^2.12.0": + version: 2.12.0 + resolution: "eslint-module-utils@npm:2.12.0" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10/bd25d6610ec3abaa50e8f1beb0119541562bbb8dd02c035c7e887976fe1e0c5dd8175f4607ca8d86d1146df24d52a071bd3d1dd329f6902bd58df805a8ca16d3 + checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee languageName: node linkType: hard "eslint-plugin-import@npm:^2.31.0": - version: 2.32.0 - resolution: "eslint-plugin-import@npm:2.32.0" + version: 2.31.0 + resolution: "eslint-plugin-import@npm:2.31.0" dependencies: "@rtsao/scc": "npm:^1.1.0" - array-includes: "npm:^3.1.9" - array.prototype.findlastindex: "npm:^1.2.6" - array.prototype.flat: "npm:^1.3.3" - array.prototype.flatmap: "npm:^1.3.3" + array-includes: "npm:^3.1.8" + array.prototype.findlastindex: "npm:^1.2.5" + array.prototype.flat: "npm:^1.3.2" + array.prototype.flatmap: "npm:^1.3.2" debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.12.1" + eslint-module-utils: "npm:^2.12.0" hasown: "npm:^2.0.2" - is-core-module: "npm:^2.16.1" + is-core-module: "npm:^2.15.1" is-glob: "npm:^4.0.3" minimatch: "npm:^3.1.2" object.fromentries: "npm:^2.0.8" object.groupby: "npm:^1.0.3" - object.values: "npm:^1.2.1" + object.values: "npm:^1.2.0" semver: "npm:^6.3.1" - string.prototype.trimend: "npm:^1.0.9" + string.prototype.trimend: "npm:^1.0.8" tsconfig-paths: "npm:^3.15.0" peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - checksum: 10/1bacf4967e9ebf99e12176a795f0d6d3a87d1c9a030c2207f27b267e10d96a1220be2647504c7fc13ab543cdf13ffef4b8f5620e0447032dba4ff0d3922f7c9e + checksum: 10/6b76bd009ac2db0615d9019699d18e2a51a86cb8c1d0855a35fb1b418be23b40239e6debdc6e8c92c59f1468ed0ea8d7b85c817117a113d5cc225be8a02ad31c languageName: node linkType: hard @@ -16374,6 +16799,15 @@ __metadata: languageName: node linkType: hard +"expand-tilde@npm:^1.2.2": + version: 1.2.2 + resolution: "expand-tilde@npm:1.2.2" + dependencies: + os-homedir: "npm:^1.0.1" + checksum: 10/18051cd104977bc06e2bb1347db9959b90504437beea0de6fd287a3c8c58b41e2330337bd189cfca2ee4be6bda9bf045f8c07daf23e622f85eb6ee1c420619a0 + languageName: node + linkType: hard + "expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2": version: 2.0.2 resolution: "expand-tilde@npm:2.0.2" @@ -16383,6 +16817,13 @@ __metadata: languageName: node linkType: hard +"expect-playwright@npm:^0.8.0": + version: 0.8.0 + resolution: "expect-playwright@npm:0.8.0" + checksum: 10/cf8f7f1db819debfb1457b2e56509f838ac4e29c832790f665cf8db7baf21af8271e0ca97849b3998040780c6848533be2afcd262c75edf1ca98805bc2b51b35 + languageName: node + linkType: hard + "expect@npm:^29.0.0, expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" @@ -16795,7 +17236,7 @@ __metadata: languageName: node linkType: hard -"find-cache-dir@npm:^3.3.1": +"find-cache-dir@npm:^3.2.0, find-cache-dir@npm:^3.3.1": version: 3.3.2 resolution: "find-cache-dir@npm:3.3.2" dependencies: @@ -16816,6 +17257,38 @@ __metadata: languageName: node linkType: hard +"find-file-up@npm:^0.1.2": + version: 0.1.3 + resolution: "find-file-up@npm:0.1.3" + dependencies: + fs-exists-sync: "npm:^0.1.0" + resolve-dir: "npm:^0.1.0" + checksum: 10/95475fee7b727266ec65312527c580eb4f01884592620296cf7859e72cce7f4f6a667c964ad6feeec53fb72a7c3991805532ed7a53d8224e9a1ccd88479cabce + languageName: node + linkType: hard + +"find-pkg@npm:^0.1.2": + version: 0.1.2 + resolution: "find-pkg@npm:0.1.2" + dependencies: + find-file-up: "npm:^0.1.2" + checksum: 10/cd797bfa7dd419849559312cdd3aec767c39939e552daa92e53ff6b61108f331eb2c800d20a5973631eb894ea36c13dded01a868b10f457a685e0ae87a1746e1 + languageName: node + linkType: hard + +"find-process@npm:^1.4.4": + version: 1.4.11 + resolution: "find-process@npm:1.4.11" + dependencies: + chalk: "npm:~4.1.2" + commander: "npm:^12.1.0" + loglevel: "npm:^1.9.2" + bin: + find-process: bin/find-process.js + checksum: 10/95f7106877c86fc09804b29339cfac518a43228c1b5f4a4e9442e57ac5538369fbeafb746fa96390ecd7be6f5a9119b58c952b5c96a0cb9f2490ef19298dec1e + languageName: node + linkType: hard + "find-root@npm:^1.1.0": version: 1.1.0 resolution: "find-root@npm:1.1.0" @@ -16986,6 +17459,16 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^2.0.0": + version: 2.0.0 + resolution: "foreground-child@npm:2.0.0" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^3.0.2" + checksum: 10/f36574ad8e19d69ce06fceac7d86161b863968e4ba292c14b7b40e5c464e3e9bcd7711250d33427d95cc2bb0d48cf101df9687433dbbc7fd3c7e4f595be8305e + languageName: node + linkType: hard + "foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" @@ -17062,6 +17545,19 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.4": + version: 4.0.4 + resolution: "form-data@npm:4.0.4" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" + mime-types: "npm:^2.1.12" + checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb + languageName: node + linkType: hard + "format@npm:^0.2.0": version: 0.2.2 resolution: "format@npm:0.2.2" @@ -17104,6 +17600,13 @@ __metadata: languageName: node linkType: hard +"fromentries@npm:^1.2.0": + version: 1.3.2 + resolution: "fromentries@npm:1.3.2" + checksum: 10/10d6e07d289db102c0c1eaf5c3e3fa55ddd6b50033d7de16d99a7cd89f1e1a302dfadb26457031f9bb5d2ed95a179aaf0396092dde5abcae06e8a2f0476826be + languageName: node + linkType: hard + "front-matter@npm:^4.0.2": version: 4.0.2 resolution: "front-matter@npm:4.0.2" @@ -17120,6 +17623,13 @@ __metadata: languageName: node linkType: hard +"fs-exists-sync@npm:^0.1.0": + version: 0.1.0 + resolution: "fs-exists-sync@npm:0.1.0" + checksum: 10/850a0d6e4c03a7bd2fd25043f77cd9d6be9c3b48bb99308bcfe9c94f3f92f65f2cd3fa036e13a1b0ba7a46d2e58792f53e578f01d75fbdcd56baeb9eed63b705 + languageName: node + linkType: hard + "fs-extra@npm:^10.0.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -17336,7 +17846,7 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 @@ -17647,7 +18157,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.3, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4": +"glob@npm:^7.0.3, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -17682,6 +18192,16 @@ __metadata: languageName: node linkType: hard +"global-modules@npm:^0.2.3": + version: 0.2.3 + resolution: "global-modules@npm:0.2.3" + dependencies: + global-prefix: "npm:^0.1.4" + is-windows: "npm:^0.2.0" + checksum: 10/3801788df54897d994c9c8f3d09f253d1379cd879ae61fcddbcc3ecdfdf6fe23a1edb983e8d4dd24cebf7e49823752e1cd29a2d33bdb4de587de8b4a85b17e24 + languageName: node + linkType: hard + "global-modules@npm:^1.0.0": version: 1.0.0 resolution: "global-modules@npm:1.0.0" @@ -17702,6 +18222,18 @@ __metadata: languageName: node linkType: hard +"global-prefix@npm:^0.1.4": + version: 0.1.5 + resolution: "global-prefix@npm:0.1.5" + dependencies: + homedir-polyfill: "npm:^1.0.0" + ini: "npm:^1.3.4" + is-windows: "npm:^0.2.0" + which: "npm:^1.2.12" + checksum: 10/ea1b818a1851655ebb2341cdd5446da81c25f31ca6f0ac358a234cbed5442edc1bfa5628771466988d67d9fcc6ad09ca0e68a8d3d7e3d92f7de3aec87020e183 + languageName: node + linkType: hard + "global-prefix@npm:^1.0.1": version: 1.0.2 resolution: "global-prefix@npm:1.0.2" @@ -18333,6 +18865,16 @@ __metadata: languageName: node linkType: hard +"hasha@npm:^5.0.0": + version: 5.2.2 + resolution: "hasha@npm:5.2.2" + dependencies: + is-stream: "npm:^2.0.0" + type-fest: "npm:^0.8.0" + checksum: 10/06cc474bed246761ff61c19d629977eb5f53fa817be4313a255a64ae0f433e831a29e83acb6555e3f4592b348497596f1d1653751008dda4f21c9c21ca60ac5a + languageName: node + linkType: hard + "hasown@npm:^2.0.0, hasown@npm:^2.0.2": version: 2.0.2 resolution: "hasown@npm:2.0.2" @@ -18474,7 +19016,7 @@ __metadata: languageName: node linkType: hard -"homedir-polyfill@npm:^1.0.1": +"homedir-polyfill@npm:^1.0.0, homedir-polyfill@npm:^1.0.1": version: 1.0.3 resolution: "homedir-polyfill@npm:1.0.3" dependencies: @@ -18655,6 +19197,20 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^3.9.2": + version: 3.10.1 + resolution: "htmlparser2@npm:3.10.1" + dependencies: + domelementtype: "npm:^1.3.1" + domhandler: "npm:^2.3.0" + domutils: "npm:^1.5.1" + entities: "npm:^1.1.1" + inherits: "npm:^2.0.1" + readable-stream: "npm:^3.1.1" + checksum: 10/d5297fe76c0d6b0f35f39781417eb560ef12fa121953578083f3f2b240c74d5c35a38185689d181b6a82b66a3025436f14aa3413b94f3cd50ba15733f2f72389 + languageName: node + linkType: hard + "htmlparser2@npm:^6.1.0": version: 6.1.0 resolution: "htmlparser2@npm:6.1.0" @@ -19435,7 +19991,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1, is-core-module@npm:^2.5.0": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0, is-core-module@npm:^2.5.0": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -19853,7 +20409,7 @@ __metadata: languageName: node linkType: hard -"is-typedarray@npm:~1.0.0": +"is-typedarray@npm:^1.0.0, is-typedarray@npm:~1.0.0": version: 1.0.0 resolution: "is-typedarray@npm:1.0.0" checksum: 10/4b433bfb0f9026f079f4eb3fbaa4ed2de17c9995c3a0b5c800bec40799b4b2a8b4e051b1ada77749deb9ded4ae52fe2096973f3a93ff83df1a5a7184a669478c @@ -19913,6 +20469,15 @@ __metadata: languageName: node linkType: hard +"is-weakref@npm:^1.1.0": + version: 1.1.0 + resolution: "is-weakref@npm:1.1.0" + dependencies: + call-bound: "npm:^1.0.2" + checksum: 10/89e627cc1763ea110574bb408fcf060ede47e70437d9278858bc939e3b3f7e4b7c558610b733da5f2ad6084d9f12b9c714b011ccf3fa771ec87e221c22bed910 + languageName: node + linkType: hard + "is-weakset@npm:^2.0.3": version: 2.0.4 resolution: "is-weakset@npm:2.0.4" @@ -19930,7 +20495,14 @@ __metadata: languageName: node linkType: hard -"is-windows@npm:^1.0.1": +"is-windows@npm:^0.2.0": + version: 0.2.0 + resolution: "is-windows@npm:0.2.0" + checksum: 10/3df25afda2fd9f3926b08cebacf1fc0a1fe7805a2cb73ef0f1b911c949e4e7648c4623979d74b4502bdd9af69471101eb6051b751595f7f88569148186cf7a7a + languageName: node + linkType: hard + +"is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" checksum: 10/438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 @@ -20023,6 +20595,27 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-hook@npm:^3.0.0": + version: 3.0.0 + resolution: "istanbul-lib-hook@npm:3.0.0" + dependencies: + append-transform: "npm:^2.0.0" + checksum: 10/512a996cce6b1b9003ba59eab42299dd1527176c01f3ceb7b16bf68f437eeab4958f9df7df0a6b258d45d5f1a2ca2a1bdb915970711e1a5d7b2de911c582f721 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^4.0.0": + version: 4.0.3 + resolution: "istanbul-lib-instrument@npm:4.0.3" + dependencies: + "@babel/core": "npm:^7.7.5" + "@istanbuljs/schema": "npm:^0.1.2" + istanbul-lib-coverage: "npm:^3.0.0" + semver: "npm:^6.3.0" + checksum: 10/6e04ab365b95644ec4954b645f901be90be8ad81233d6df536300cdafcf70dd1ed22a912ceda38b32053c7fc9830c44cd23550c603f493329a8532073d1d6c42 + languageName: node + linkType: hard + "istanbul-lib-instrument@npm:^5.0.4": version: 5.1.0 resolution: "istanbul-lib-instrument@npm:5.1.0" @@ -20049,6 +20642,20 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-processinfo@npm:^2.0.2": + version: 2.0.3 + resolution: "istanbul-lib-processinfo@npm:2.0.3" + dependencies: + archy: "npm:^1.0.0" + cross-spawn: "npm:^7.0.3" + istanbul-lib-coverage: "npm:^3.2.0" + p-map: "npm:^3.0.0" + rimraf: "npm:^3.0.0" + uuid: "npm:^8.3.2" + checksum: 10/60e7b3441687249460f34a817c7204967b07830a69b6e430e60a45615319c2ab4e2b2eaeb8b3decf549fccd419cd600d21173961632229967608d7d1b194f39e + languageName: node + linkType: hard + "istanbul-lib-report@npm:^3.0.0": version: 3.0.0 resolution: "istanbul-lib-report@npm:3.0.0" @@ -20071,6 +20678,16 @@ __metadata: languageName: node linkType: hard +"istanbul-reports@npm:^3.0.2": + version: 3.1.7 + resolution: "istanbul-reports@npm:3.1.7" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10/f1faaa4684efaf57d64087776018d7426312a59aa6eeb4e0e3a777347d23cd286ad18f427e98f0e3dee666103d7404c9d7abc5f240406a912fa16bd6695437fa + languageName: node + linkType: hard + "istanbul-reports@npm:^3.1.3": version: 3.1.5 resolution: "istanbul-reports@npm:3.1.5" @@ -20162,7 +20779,7 @@ __metadata: languageName: node linkType: hard -"jest-circus@npm:^29.7.0": +"jest-circus@npm:^29.6.4, jest-circus@npm:^29.7.0": version: 29.7.0 resolution: "jest-circus@npm:29.7.0" dependencies: @@ -20328,7 +20945,7 @@ __metadata: languageName: node linkType: hard -"jest-environment-node@npm:^29.7.0": +"jest-environment-node@npm:^29.6.4, jest-environment-node@npm:^29.7.0": version: 29.7.0 resolution: "jest-environment-node@npm:29.7.0" dependencies: @@ -20386,7 +21003,7 @@ __metadata: languageName: node linkType: hard -"jest-junit@npm:16.0.0": +"jest-junit@npm:16.0.0, jest-junit@npm:^16.0.0": version: 16.0.0 resolution: "jest-junit@npm:16.0.0" dependencies: @@ -20448,6 +21065,25 @@ __metadata: languageName: node linkType: hard +"jest-playwright-preset@npm:^4.0.0": + version: 4.0.0 + resolution: "jest-playwright-preset@npm:4.0.0" + dependencies: + expect-playwright: "npm:^0.8.0" + jest-process-manager: "npm:^0.4.0" + nyc: "npm:^15.1.0" + playwright-core: "npm:>=1.2.0" + rimraf: "npm:^3.0.2" + uuid: "npm:^8.3.2" + peerDependencies: + jest: ^29.3.1 + jest-circus: ^29.3.1 + jest-environment-node: ^29.3.1 + jest-runner: ^29.3.1 + checksum: 10/ad8b92b4eac452042d71666f6fa7ce56abd590ec12732ec7c08926ff48351f72ea07a3babb4843db9e2e5e0c3a01f0d22da6acb1eaadf9a43c84c368ba188127 + languageName: node + linkType: hard + "jest-pnp-resolver@npm:^1.2.2": version: 1.2.2 resolution: "jest-pnp-resolver@npm:1.2.2" @@ -20460,6 +21096,24 @@ __metadata: languageName: node linkType: hard +"jest-process-manager@npm:^0.4.0": + version: 0.4.0 + resolution: "jest-process-manager@npm:0.4.0" + dependencies: + "@types/wait-on": "npm:^5.2.0" + chalk: "npm:^4.1.0" + cwd: "npm:^0.10.0" + exit: "npm:^0.1.2" + find-process: "npm:^1.4.4" + prompts: "npm:^2.4.1" + signal-exit: "npm:^3.0.3" + spawnd: "npm:^5.0.0" + tree-kill: "npm:^1.2.2" + wait-on: "npm:^7.0.0" + checksum: 10/5d69465a497d1ba0afe7a8241c4819a6f88dfbc22355b89d8ca0a6a39fbe8c4f57dbfff5697f1875f05b117276261e2eb8cc3bbf3395e911646199dae46cdb31 + languageName: node + linkType: hard + "jest-regex-util@npm:30.0.1": version: 30.0.1 resolution: "jest-regex-util@npm:30.0.1" @@ -20501,7 +21155,7 @@ __metadata: languageName: node linkType: hard -"jest-runner@npm:^29.7.0": +"jest-runner@npm:^29.6.4, jest-runner@npm:^29.7.0": version: 29.7.0 resolution: "jest-runner@npm:29.7.0" dependencies: @@ -20560,6 +21214,15 @@ __metadata: languageName: node linkType: hard +"jest-serializer-html@npm:^7.1.0": + version: 7.1.0 + resolution: "jest-serializer-html@npm:7.1.0" + dependencies: + diffable-html: "npm:^4.1.0" + checksum: 10/64f9139a3ffb2971c8d03d41c543cb03633532dfa0d6a7d6017646556965a8549f7886452d942124181b24e2fecb9131a49c56f3a3ab1047bac86cbe24419894 + languageName: node + linkType: hard + "jest-snapshot@npm:^29.7.0": version: 29.7.0 resolution: "jest-snapshot@npm:29.7.0" @@ -20616,7 +21279,7 @@ __metadata: languageName: node linkType: hard -"jest-watch-typeahead@npm:^2.2.2": +"jest-watch-typeahead@npm:^2.0.0, jest-watch-typeahead@npm:^2.2.2": version: 2.2.2 resolution: "jest-watch-typeahead@npm:2.2.2" dependencies: @@ -20742,6 +21405,19 @@ __metadata: languageName: node linkType: hard +"joi@npm:^17.11.0": + version: 17.13.3 + resolution: "joi@npm:17.13.3" + dependencies: + "@hapi/hoek": "npm:^9.3.0" + "@hapi/topo": "npm:^5.1.0" + "@sideway/address": "npm:^4.1.5" + "@sideway/formula": "npm:^3.0.1" + "@sideway/pinpoint": "npm:^2.0.0" + checksum: 10/4c150db0c820c3a52f4a55c82c1fc5e144a5b5f4da9ffebc7339a15469d1a447ebb427ced446efcb9709ab56bd71a06c4c67c9381bc1b9f9ae63fc7c89209bdf + languageName: node + linkType: hard + "jpeg-js@npm:^0.4.4": version: 0.4.4 resolution: "jpeg-js@npm:0.4.4" @@ -21624,6 +22300,13 @@ __metadata: languageName: node linkType: hard +"lodash.flattendeep@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.flattendeep@npm:4.4.0" + checksum: 10/0d0b41d8d86999e8bea94905ac65347404d427aacddbc6654dc2f85905e27cd2b708139671ecea135fa6f0a17ed94b9d4cab8ce12b08eddcbb1ddd83952ee4c2 + languageName: node + linkType: hard + "lodash.get@npm:^4.4.2": version: 4.4.2 resolution: "lodash.get@npm:4.4.2" @@ -21773,6 +22456,13 @@ __metadata: languageName: node linkType: hard +"loglevel@npm:^1.9.2": + version: 1.9.2 + resolution: "loglevel@npm:1.9.2" + checksum: 10/6153d8db308323f7ee20130bc40309e7a976c30a10379d8666b596d9c6441965c3e074c8d7ee3347fe5cfc059c0375b6f3e8a10b93d5b813cc5547f5aa412a29 + languageName: node + linkType: hard + "long@npm:^5.0.0": version: 5.2.3 resolution: "long@npm:5.2.3" @@ -23191,6 +23881,15 @@ __metadata: languageName: node linkType: hard +"node-preload@npm:^0.2.1": + version: 0.2.1 + resolution: "node-preload@npm:0.2.1" + dependencies: + process-on-spawn: "npm:^1.0.0" + checksum: 10/de36ed365b7e474eaf05c41f976774dece23a7f398fe76dbf9705f9670a1f49e6a27c5f31fe58b4e43d96413fdce4806192c60d35317b25725636c90889d5bab + languageName: node + linkType: hard + "node-readfiles@npm:^0.2.0": version: 0.2.0 resolution: "node-readfiles@npm:0.2.0" @@ -23506,6 +24205,43 @@ __metadata: languageName: node linkType: hard +"nyc@npm:^15.1.0": + version: 15.1.0 + resolution: "nyc@npm:15.1.0" + dependencies: + "@istanbuljs/load-nyc-config": "npm:^1.0.0" + "@istanbuljs/schema": "npm:^0.1.2" + caching-transform: "npm:^4.0.0" + convert-source-map: "npm:^1.7.0" + decamelize: "npm:^1.2.0" + find-cache-dir: "npm:^3.2.0" + find-up: "npm:^4.1.0" + foreground-child: "npm:^2.0.0" + get-package-type: "npm:^0.1.0" + glob: "npm:^7.1.6" + istanbul-lib-coverage: "npm:^3.0.0" + istanbul-lib-hook: "npm:^3.0.0" + istanbul-lib-instrument: "npm:^4.0.0" + istanbul-lib-processinfo: "npm:^2.0.2" + istanbul-lib-report: "npm:^3.0.0" + istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-reports: "npm:^3.0.2" + make-dir: "npm:^3.0.0" + node-preload: "npm:^0.2.1" + p-map: "npm:^3.0.0" + process-on-spawn: "npm:^1.0.0" + resolve-from: "npm:^5.0.0" + rimraf: "npm:^3.0.0" + signal-exit: "npm:^3.0.2" + spawn-wrap: "npm:^2.0.0" + test-exclude: "npm:^6.0.0" + yargs: "npm:^15.0.2" + bin: + nyc: bin/nyc.js + checksum: 10/c987f04f4192dfd94e9e69869c76a54220b3ed555016751f380a413a378cceff8ec346df579e9126035b6acbc60ab893cc65e67729cc427c0171361bcb481e66 + languageName: node + linkType: hard + "oas-kit-common@npm:^1.0.8": version: 1.0.8 resolution: "oas-kit-common@npm:1.0.8" @@ -23709,7 +24445,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.6, object.values@npm:^1.2.1": +"object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1": version: 1.2.1 resolution: "object.values@npm:1.2.1" dependencies: @@ -23911,6 +24647,13 @@ __metadata: languageName: node linkType: hard +"os-homedir@npm:^1.0.1": + version: 1.0.2 + resolution: "os-homedir@npm:1.0.2" + checksum: 10/af609f5a7ab72de2f6ca9be6d6b91a599777afc122ac5cad47e126c1f67c176fe9b52516b9eeca1ff6ca0ab8587fe66208bc85e40a3940125f03cdb91408e9d2 + languageName: node + linkType: hard + "os-tmpdir@npm:~1.0.2": version: 1.0.2 resolution: "os-tmpdir@npm:1.0.2" @@ -24047,6 +24790,15 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^3.0.0": + version: 3.0.0 + resolution: "p-map@npm:3.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 10/d4a0664d2af05d7e5f6f342e6493d4cad48f7398ac803c5066afb1f8d2010bfc2a83d935689437288f7b1a743772085b8fa0909a8282b5df4210bcda496c37c8 + languageName: node + linkType: hard + "p-map@npm:^5.5.0": version: 5.5.0 resolution: "p-map@npm:5.5.0" @@ -24161,6 +24913,18 @@ __metadata: languageName: node linkType: hard +"package-hash@npm:^4.0.0": + version: 4.0.0 + resolution: "package-hash@npm:4.0.0" + dependencies: + graceful-fs: "npm:^4.1.15" + hasha: "npm:^5.0.0" + lodash.flattendeep: "npm:^4.4.0" + release-zalgo: "npm:^1.0.0" + checksum: 10/c7209d98ac31926e0c1753d014f8b6b924e1e6a1aacf833dc99edece9c8381424c41c97c26c7eee82026944a79e99023cde5998bf515d7465c87005d52152040 + languageName: node + linkType: hard + "package-json-from-dist@npm:^1.0.0": version: 1.0.0 resolution: "package-json-from-dist@npm:1.0.0" @@ -24772,7 +25536,7 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.54.1": +"playwright-core@npm:1.54.1, playwright-core@npm:>=1.2.0": version: 1.54.1 resolution: "playwright-core@npm:1.54.1" bin: @@ -24781,7 +25545,7 @@ __metadata: languageName: node linkType: hard -"playwright@npm:1.54.1": +"playwright@npm:1.54.1, playwright@npm:^1.14.0": version: 1.54.1 resolution: "playwright@npm:1.54.1" dependencies: @@ -25428,6 +26192,15 @@ __metadata: languageName: node linkType: hard +"process-on-spawn@npm:^1.0.0": + version: 1.1.0 + resolution: "process-on-spawn@npm:1.1.0" + dependencies: + fromentries: "npm:^1.2.0" + checksum: 10/4cc56df51bf54d7629c1857e472c9440984d230c4a4dfdfc2de25abcee57b3d8f4bdfb0b9ad65fe7eea11a7a10f03474c3e8c5eb554454d32c86444e635c85f8 + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -25487,7 +26260,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.0.1": +"prompts@npm:^2.0.1, prompts@npm:^2.4.1": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -27280,6 +28053,15 @@ __metadata: languageName: node linkType: hard +"release-zalgo@npm:^1.0.0": + version: 1.0.0 + resolution: "release-zalgo@npm:1.0.0" + dependencies: + es6-error: "npm:^4.0.1" + checksum: 10/1719e44b240ee1f57d034b26ea167f3cbf3c36fdae6d6efd0e6e5b202d9852baffc1c5595d378b5f8b2ad729b907ddd962f3d051d89499f83584993a5399f964 + languageName: node + linkType: hard + "reliable-random@npm:^0.0.1": version: 0.0.1 resolution: "reliable-random@npm:0.0.1" @@ -27374,6 +28156,13 @@ __metadata: languageName: node linkType: hard +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: 10/8604a570c06a69c9d939275becc33a65676529e1c3e5a9f42d58471674df79357872b96d70bb93a0380a62d60dc9031c98b1a9dad98c946ffdd61b7ac0c8cedd + languageName: node + linkType: hard + "requireindex@npm:^1.2.0": version: 1.2.0 resolution: "requireindex@npm:1.2.0" @@ -27411,6 +28200,16 @@ __metadata: languageName: node linkType: hard +"resolve-dir@npm:^0.1.0": + version: 0.1.1 + resolution: "resolve-dir@npm:0.1.1" + dependencies: + expand-tilde: "npm:^1.2.2" + global-modules: "npm:^0.2.3" + checksum: 10/cc3e1885938f8fe9656a6faa651e21730d369260e907b8dd7c847a4aa18db348ac08ee0dbf2d6f87e2ba08715fb109432ec773bbb31698381bd2a48c0ea66072 + languageName: node + linkType: hard + "resolve-dir@npm:^1.0.0, resolve-dir@npm:^1.0.1": version: 1.0.1 resolution: "resolve-dir@npm:1.0.1" @@ -27597,7 +28396,7 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^3.0.2": +"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" dependencies: @@ -28855,6 +29654,32 @@ __metadata: languageName: node linkType: hard +"spawn-wrap@npm:^2.0.0": + version: 2.0.0 + resolution: "spawn-wrap@npm:2.0.0" + dependencies: + foreground-child: "npm:^2.0.0" + is-windows: "npm:^1.0.2" + make-dir: "npm:^3.0.0" + rimraf: "npm:^3.0.0" + signal-exit: "npm:^3.0.2" + which: "npm:^2.0.1" + checksum: 10/ce6ca08d66c3a41a28a7ecc10bf4945d7930fd3ae961d40804ee109cee6ee9f8436125f53bc07918ca1eb461fe2ff0033af1dc3cb803469b585639675fc2d2e7 + languageName: node + linkType: hard + +"spawnd@npm:^5.0.0": + version: 5.0.0 + resolution: "spawnd@npm:5.0.0" + dependencies: + exit: "npm:^0.1.2" + signal-exit: "npm:^3.0.3" + tree-kill: "npm:^1.2.2" + wait-port: "npm:^0.2.9" + checksum: 10/6b1a8e4096efbf77729818ba09b9a54a3f6f9c002dfd62610f6d35ea80b21bd4a9c39a3d86650440ccd364f6bc2f46da1fa292836fbb1e9af02ca64a58990184 + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.1.1 resolution: "spdx-correct@npm:3.1.1" @@ -29292,7 +30117,7 @@ __metadata: languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.9": +"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9": version: 1.0.9 resolution: "string.prototype.trimend@npm:1.0.9" dependencies: @@ -30213,7 +31038,7 @@ __metadata: languageName: node linkType: hard -"tree-kill@npm:1.2.2": +"tree-kill@npm:1.2.2, tree-kill@npm:^1.2.2": version: 1.2.2 resolution: "tree-kill@npm:1.2.2" bin: @@ -30565,7 +31390,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.8.1": +"type-fest@npm:^0.8.0, type-fest@npm:^0.8.1": version: 0.8.1 resolution: "type-fest@npm:0.8.1" checksum: 10/fd4a91bfb706aeeb0d326ebd2e9a8ea5263979e5dec8d16c3e469a5bd3a946e014a062ef76c02e3086d3d1c7209a56a20a4caafd0e9f9a5c2ab975084ea3d388 @@ -30667,6 +31492,15 @@ __metadata: languageName: node linkType: hard +"typedarray-to-buffer@npm:^3.1.5": + version: 3.1.5 + resolution: "typedarray-to-buffer@npm:3.1.5" + dependencies: + is-typedarray: "npm:^1.0.0" + checksum: 10/7c850c3433fbdf4d04f04edfc751743b8f577828b8e1eb93b95a3bce782d156e267d83e20fb32b3b47813e69a69ab5e9b5342653332f7d21c7d1210661a7a72c + languageName: node + linkType: hard + "typedarray@npm:^0.0.6": version: 0.0.6 resolution: "typedarray@npm:0.0.6" @@ -31385,6 +32219,34 @@ __metadata: languageName: node linkType: hard +"wait-on@npm:^7.0.0": + version: 7.2.0 + resolution: "wait-on@npm:7.2.0" + dependencies: + axios: "npm:^1.6.1" + joi: "npm:^17.11.0" + lodash: "npm:^4.17.21" + minimist: "npm:^1.2.8" + rxjs: "npm:^7.8.1" + bin: + wait-on: bin/wait-on + checksum: 10/00299e3b651c70d7082d02b93d9d4784cbe851914f1674d795d578d4826876193fdc7bee7e9491264b7c2d242ac9fe6e1fd09e1143409f730f13a7ee2da67fff + languageName: node + linkType: hard + +"wait-port@npm:^0.2.9": + version: 0.2.14 + resolution: "wait-port@npm:0.2.14" + dependencies: + chalk: "npm:^2.4.2" + commander: "npm:^3.0.2" + debug: "npm:^4.1.1" + bin: + wait-port: bin/wait-port.js + checksum: 10/3a970828f6b9080d05bbe39ea6c0546ab7b455fec65b63fc704cdcbccb4d83c66815ef3bc2e0e123aab88a2afb70532c5697af27284b36bd03cb06cd446066a8 + languageName: node + linkType: hard + "walk-sync@npm:^2.2.0": version: 2.2.0 resolution: "walk-sync@npm:2.2.0" @@ -31898,6 +32760,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.1 + resolution: "which-module@npm:2.0.1" + checksum: 10/1967b7ce17a2485544a4fdd9063599f0f773959cca24176dbe8f405e55472d748b7c549cd7920ff6abb8f1ab7db0b0f1b36de1a21c57a8ff741f4f1e792c52be + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19, which-typed-array@npm:^1.1.2": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" @@ -31913,7 +32782,21 @@ __metadata: languageName: node linkType: hard -"which@npm:^1.2.14, which@npm:^1.3.1": +"which-typed-array@npm:^1.1.18": + version: 1.1.18 + resolution: "which-typed-array@npm:1.1.18" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.3" + for-each: "npm:^0.3.3" + gopd: "npm:^1.2.0" + has-tostringtag: "npm:^1.0.2" + checksum: 10/11eed801b2bd08cdbaecb17aff381e0fb03526532f61acc06e6c7b9370e08062c33763a51f27825f13fdf34aabd0df6104007f4e8f96e6eaef7db0ce17a26d6e + languageName: node + linkType: hard + +"which@npm:^1.2.12, which@npm:^1.2.14, which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" dependencies: @@ -32030,6 +32913,18 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^3.0.0": + version: 3.0.3 + resolution: "write-file-atomic@npm:3.0.3" + dependencies: + imurmurhash: "npm:^0.1.4" + is-typedarray: "npm:^1.0.0" + signal-exit: "npm:^3.0.2" + typedarray-to-buffer: "npm:^3.1.5" + checksum: 10/0955ab94308b74d32bc252afe69d8b42ba4b8a28b8d79f399f3f405969f82623f981e35d13129a52aa2973450f342107c06d86047572637584e85a1c0c246bf3 + languageName: node + linkType: hard + "write-file-atomic@npm:^4.0.2": version: 4.0.2 resolution: "write-file-atomic@npm:4.0.2" @@ -32199,6 +33094,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 10/392870b2a100bbc643bc035fe3a89cef5591b719c7bdc8721bcdb3d27ab39fa4870acdca67b0ee096e146d769f311d68eda6b8195a6d970f227795061923013f + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -32243,6 +33145,16 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: "npm:^5.0.0" + decamelize: "npm:^1.2.0" + checksum: 10/235bcbad5b7ca13e5abc54df61d42f230857c6f83223a38e4ed7b824681875b7f8b6ed52139d88a3ad007050f28dc0324b3c805deac7db22ae3b4815dae0e1bf + languageName: node + linkType: hard + "yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.3": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" @@ -32265,6 +33177,25 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^15.0.2": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + dependencies: + cliui: "npm:^6.0.0" + decamelize: "npm:^1.2.0" + find-up: "npm:^4.1.0" + get-caller-file: "npm:^2.0.1" + require-directory: "npm:^2.1.1" + require-main-filename: "npm:^2.0.0" + set-blocking: "npm:^2.0.0" + string-width: "npm:^4.2.0" + which-module: "npm:^2.0.0" + y18n: "npm:^4.0.0" + yargs-parser: "npm:^18.1.2" + checksum: 10/bbcc82222996c0982905b668644ca363eebe6ffd6a572fbb52f0c0e8146661d8ce5af2a7df546968779bb03d1e4186f3ad3d55dfaadd1c4f0d5187c0e3a5ba16 + languageName: node + linkType: hard + "yargs@npm:^16.0.0, yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" From e74b4456e423011be9a47c2339dabd2dee070cf8 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:51:44 +0200 Subject: [PATCH 066/101] Update developer-guide.md for running decoupled plugins (#108604) * Update developer-guide for running decoupled plugins * Update contribute/developer-guide.md Co-authored-by: Jack Westbrook * Fix lint --------- Co-authored-by: Jack Westbrook --- contribute/developer-guide.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 6ed40a72edb..45805f71fb3 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -101,11 +101,12 @@ If you want to contribute to any of the plugins listed below (that are found wit - grafana-postgresql-datasource - grafana-pyroscope-datasource - grafana-testdata-datasource -- jaegar +- jaeger - mysql - parca - tempo - zipkin +- loki To build and watch all these plugins you can run the following command. Note this can be quite resource intensive as it will start separate build processes for each plugin. @@ -119,6 +120,16 @@ If, instead, you would like to build and watch a specific plugin you can run the yarn workspace dev ``` +If you want to run multiple specific plugins, you can use the following command. + +``` +yarn nx run-many -t dev --projects="@grafana-plugins/grafana-azure-monitor-datasource,@grafana-plugins/jaeger" +``` + +If you're unsure of the name of the plugins you'd like to run you can query nx with the following command to get a list of all plugins: + +`yarn nx show projects --projects="@grafana-plugins/*"` + Next, we'll explain how to build and run the web server that serves these frontend assets. ### Backend From 915f47befd4ecddf142d17f3bb0071836e3aa03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Thu, 24 Jul 2025 17:57:02 +0200 Subject: [PATCH 067/101] Provisioning: commit only once staged changes (#108590) --- pkg/apis/provisioning/v0alpha1/jobs.go | 6 + .../v0alpha1/zz_generated.openapi.go | 14 + .../apis/provisioning/jobs/export/worker.go | 10 +- .../provisioning/jobs/export/worker_test.go | 4 +- .../apis/provisioning/jobs/migrate/legacy.go | 12 +- .../jobs/migrate/unifiedstorage.go | 4 +- .../repository/git/repository_test.go | 2 +- .../provisioning/repository/git/staged.go | 69 +++-- .../repository/git/staged_test.go | 239 ++++++++++++++---- .../repository/github/repository_test.go | 4 +- .../apis/provisioning/repository/staged.go | 18 +- .../provisioning.grafana.app-v0alpha1.json | 8 + .../provisioning/v0alpha1/endpoints.gen.ts | 4 + 13 files changed, 295 insertions(+), 99 deletions(-) diff --git a/pkg/apis/provisioning/v0alpha1/jobs.go b/pkg/apis/provisioning/v0alpha1/jobs.go index f4fa20e8934..5d0348864a7 100644 --- a/pkg/apis/provisioning/v0alpha1/jobs.go +++ b/pkg/apis/provisioning/v0alpha1/jobs.go @@ -103,6 +103,9 @@ type SyncJobOptions struct { } type ExportJobOptions struct { + // Message to use when committing the changes in a single commit + Message string `json:"message,omitempty"` + // The source folder (or empty) to export Folder string `json:"folder,omitempty"` @@ -116,6 +119,9 @@ type ExportJobOptions struct { type MigrateJobOptions struct { // Preserve history (if possible) History bool `json:"history,omitempty"` + + // Message to use when committing the changes in a single commit + Message string `json:"message,omitempty"` } // The job status diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 58a67095b53..737d2cc29c0 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -193,6 +193,13 @@ func schema_pkg_apis_provisioning_v0alpha1_ExportJobOptions(ref common.Reference SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ + "message": { + SchemaProps: spec.SchemaProps{ + Description: "Message to use when committing the changes in a single commit", + Type: []string{"string"}, + Format: "", + }, + }, "folder": { SchemaProps: spec.SchemaProps{ Description: "The source folder (or empty) to export", @@ -1030,6 +1037,13 @@ func schema_pkg_apis_provisioning_v0alpha1_MigrateJobOptions(ref common.Referenc Format: "", }, }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "Message to use when committing the changes in a single commit", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go index 5ded8555257..87405a881ef 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker.go @@ -56,9 +56,15 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, return err } + msg := options.Message + if msg == "" { + msg = fmt.Sprintf("Export from Grafana %s", job.Name) + } + cloneOptions := repository.StageOptions{ - Timeout: 10 * time.Minute, - PushOnWrites: false, + Mode: repository.StageModeCommitOnlyOnce, + CommitOnlyOnceMessage: msg, + Timeout: 10 * time.Minute, } fn := func(repo repository.Repository, _ bool) error { diff --git a/pkg/registry/apis/provisioning/jobs/export/worker_test.go b/pkg/registry/apis/provisioning/jobs/export/worker_test.go index aa5f2ea1c5e..af419538a42 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker_test.go @@ -260,7 +260,7 @@ func TestExportWorker_ProcessStageOptions(t *testing.T) { mockStageFn := NewMockWrapWithStageFn(t) // Verify clone and push options mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { - return opts.Timeout == 10*time.Minute && !opts.PushOnWrites + return opts.Timeout == 10*time.Minute && opts.Mode == repository.StageModeCommitOnlyOnce }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) @@ -406,7 +406,7 @@ func TestExportWorker_ProcessGitRepository(t *testing.T) { mockStageFn := NewMockWrapWithStageFn(t) // Verify clone and push options mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { - return opts.Timeout == 10*time.Minute && !opts.PushOnWrites + return opts.Timeout == 10*time.Minute && opts.Mode == repository.StageModeCommitOnlyOnce }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) diff --git a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go index f2a78b5181e..8b253bebd7f 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/legacy.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/legacy.go @@ -35,8 +35,18 @@ func NewLegacyMigrator( func (m *LegacyMigrator) Migrate(ctx context.Context, rw repository.ReaderWriter, options provisioning.MigrateJobOptions, progress jobs.JobProgressRecorder) error { namespace := rw.Config().Namespace + var stageMode repository.StageMode + if options.History { + // When History is true, we want to commit and push each file (previous PushOnWrites: true) + stageMode = repository.StageModeCommitAndPushOnEach + } else { + // When History is false, we want to commit only once (previous CommitOnlyOnce: true) + stageMode = repository.StageModeCommitOnlyOnce + } + stageOptions := repository.StageOptions{ - PushOnWrites: options.History, + Mode: stageMode, + CommitOnlyOnceMessage: options.Message, // TODO: make this configurable Timeout: 10 * time.Minute, } diff --git a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go index d0b9739d72e..4f9f33b96ec 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/unifiedstorage.go @@ -37,7 +37,9 @@ func (m *UnifiedStorageMigrator) Migrate(ctx context.Context, repo repository.Re exportJob := provisioning.Job{ Spec: provisioning.JobSpec{ - Push: &provisioning.ExportJobOptions{}, + Push: &provisioning.ExportJobOptions{ + Message: options.Message, + }, }, } if err := m.exportWorker.Process(ctx, repo, exportJob, progress); err != nil { diff --git a/pkg/registry/apis/provisioning/repository/git/repository_test.go b/pkg/registry/apis/provisioning/repository/git/repository_test.go index 0f13c2f9854..2c731b4c9f3 100644 --- a/pkg/registry/apis/provisioning/repository/git/repository_test.go +++ b/pkg/registry/apis/provisioning/repository/git/repository_test.go @@ -2256,7 +2256,7 @@ func TestGitRepository_Stage(t *testing.T) { t.Run("calls NewStagedGitRepository", func(t *testing.T) { ctx := context.Background() opts := repository.StageOptions{ - PushOnWrites: true, + Mode: repository.StageModeCommitAndPushOnEach, } // Since NewStagedGitRepository is not mocked and may panic, we expect this to fail diff --git a/pkg/registry/apis/provisioning/repository/git/staged.go b/pkg/registry/apis/provisioning/repository/git/staged.go index e11044230b2..e122bc8d33b 100644 --- a/pkg/registry/apis/provisioning/repository/git/staged.go +++ b/pkg/registry/apis/provisioning/repository/git/staged.go @@ -69,6 +69,25 @@ func (r *stagedGitRepository) ReadTree(ctx context.Context, ref string) ([]repos return r.gitRepository.ReadTree(ctx, ref) } +// handleCommitAndPush handles the commit and push logic based on the StageMode +func (r *stagedGitRepository) handleCommitAndPush(ctx context.Context, message string) error { + switch r.opts.Mode { + case repository.StageModeCommitOnEach: + return r.commit(ctx, r.writer, message) + case repository.StageModeCommitAndPushOnEach: + if err := r.commit(ctx, r.writer, message); err != nil { + return err + } + return r.Push(ctx) + case repository.StageModeCommitOnlyOnce: + // No immediate commit, will commit on Push + return nil + default: + // Default to StageModeCommitOnEach for backward compatibility + return r.commit(ctx, r.writer, message) + } +} + func (r *stagedGitRepository) Create(ctx context.Context, path, ref string, data []byte, message string) error { if ref != "" && ref != r.gitConfig.Branch { return errors.New("ref is not supported for staged repository") @@ -78,15 +97,7 @@ func (r *stagedGitRepository) Create(ctx context.Context, path, ref string, data return err } - if err := r.commit(ctx, r.writer, message); err != nil { - return err - } - - if r.opts.PushOnWrites { - return r.Push(ctx) - } - - return nil + return r.handleCommitAndPush(ctx, message) } func (r *stagedGitRepository) blobExists(ctx context.Context, path string) (bool, error) { @@ -116,15 +127,7 @@ func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data } } - if err := r.commit(ctx, r.writer, message); err != nil { - return err - } - - if r.opts.PushOnWrites { - return r.Push(ctx) - } - - return nil + return r.handleCommitAndPush(ctx, message) } func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data []byte, message string) error { @@ -140,15 +143,7 @@ func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data return err } - if err := r.commit(ctx, r.writer, message); err != nil { - return err - } - - if r.opts.PushOnWrites { - return r.Push(ctx) - } - - return nil + return r.handleCommitAndPush(ctx, message) } func (r *stagedGitRepository) Delete(ctx context.Context, path, ref, message string) error { @@ -160,15 +155,7 @@ func (r *stagedGitRepository) Delete(ctx context.Context, path, ref, message str return err } - if err := r.commit(ctx, r.writer, message); err != nil { - return err - } - - if r.opts.PushOnWrites { - return r.Push(ctx) - } - - return nil + return r.handleCommitAndPush(ctx, message) } func (r *stagedGitRepository) Push(ctx context.Context) error { @@ -178,6 +165,16 @@ func (r *stagedGitRepository) Push(ctx context.Context) error { defer cancel() } + if r.opts.Mode == repository.StageModeCommitOnlyOnce { + message := r.opts.CommitOnlyOnceMessage + if message == "" { + message = "Staged changes" + } + if err := r.commit(ctx, r.writer, message); err != nil { + return err + } + } + return r.writer.Push(ctx) } diff --git a/pkg/registry/apis/provisioning/repository/git/staged_test.go b/pkg/registry/apis/provisioning/repository/git/staged_test.go index 9dc502cf8c2..73604e72fa3 100644 --- a/pkg/registry/apis/provisioning/repository/git/staged_test.go +++ b/pkg/registry/apis/provisioning/repository/git/staged_test.go @@ -3,6 +3,7 @@ package git import ( "context" "errors" + "strings" "testing" "time" @@ -32,7 +33,7 @@ func TestNewStagedGitRepository(t *testing.T) { mockClient.NewStagedWriterReturns(mockWriter, nil) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, wantError: nil, }, @@ -47,7 +48,7 @@ func TestNewStagedGitRepository(t *testing.T) { mockClient.NewStagedWriterReturns(mockWriter, nil) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, wantError: nil, }, @@ -62,8 +63,39 @@ func TestNewStagedGitRepository(t *testing.T) { mockClient.NewStagedWriterReturns(mockWriter, nil) }, opts: repository.StageOptions{ - PushOnWrites: false, - Timeout: time.Second * 5, + Mode: repository.StageModeCommitOnEach, + Timeout: time.Second * 5, + }, + wantError: nil, + }, + { + name: "succeeds with CommitOnlyOnce option", + setupMock: func(mockClient *mocks.FakeClient) { + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: hash.Hash{1, 2, 3}, + }, nil) + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + }, + opts: repository.StageOptions{ + Mode: repository.StageModeCommitOnlyOnce, + CommitOnlyOnceMessage: "Custom commit message", + }, + wantError: nil, + }, + { + name: "succeeds with CommitAndPushOnEach option", + setupMock: func(mockClient *mocks.FakeClient) { + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: hash.Hash{1, 2, 3}, + }, nil) + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + }, + opts: repository.StageOptions{ + Mode: repository.StageModeCommitAndPushOnEach, }, wantError: nil, }, @@ -73,7 +105,7 @@ func TestNewStagedGitRepository(t *testing.T) { mockClient.GetRefReturns(nanogit.Ref{}, errors.New("ref not found")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, wantError: errors.New("ref not found"), }, @@ -87,7 +119,7 @@ func TestNewStagedGitRepository(t *testing.T) { mockClient.NewStagedWriterReturns(nil, errors.New("failed to create writer")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, wantError: errors.New("build staged writer: failed to create writer"), }, @@ -121,8 +153,9 @@ func TestNewStagedGitRepository(t *testing.T) { // Compare opts fields individually since function pointers can't be compared directly actualOpts := stagedRepo.(*stagedGitRepository).opts - require.Equal(t, tt.opts.PushOnWrites, actualOpts.PushOnWrites) + require.Equal(t, tt.opts.Mode, actualOpts.Mode) require.Equal(t, tt.opts.Timeout, actualOpts.Timeout) + require.Equal(t, tt.opts.CommitOnlyOnceMessage, actualOpts.CommitOnlyOnceMessage) } }) } @@ -277,7 +310,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -294,7 +327,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.PushReturns(nil) }, opts: repository.StageOptions{ - PushOnWrites: true, + Mode: repository.StageModeCommitAndPushOnEach, }, path: "test.yaml", ref: "main", @@ -309,7 +342,7 @@ func TestStagedGitRepository_Create(t *testing.T) { // No setup needed as error occurs before writer calls }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "feature-branch", @@ -323,7 +356,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create blob failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -338,7 +371,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -354,7 +387,7 @@ func TestStagedGitRepository_Create(t *testing.T) { mockWriter.PushReturns(errors.New("push failed")) }, opts: repository.StageOptions{ - PushOnWrites: true, + Mode: repository.StageModeCommitAndPushOnEach, }, path: "test.yaml", ref: "", @@ -363,6 +396,21 @@ func TestStagedGitRepository_Create(t *testing.T) { wantError: errors.New("push failed"), expectPush: true, // Push is still called even though it fails }, + { + name: "succeeds with CommitOnlyOnce - no immediate commit", + setupMock: func(mockWriter *mocks.FakeStagedWriter) { + mockWriter.CreateBlobReturns(hash.Hash{1, 2, 3}, nil) + }, + opts: repository.StageOptions{ + Mode: repository.StageModeCommitOnlyOnce, + }, + path: "test.yaml", + ref: "", + data: []byte("content"), + message: "Create test file", + wantError: nil, + expectPush: false, + }, } for _, tt := range tests { @@ -386,6 +434,28 @@ func TestStagedGitRepository_Create(t *testing.T) { } else if tt.wantError == nil { require.Equal(t, 0, mockWriter.PushCallCount()) } + + // Verify commit behavior based on StageMode + switch tt.opts.Mode { + case repository.StageModeCommitOnlyOnce: + require.Equal(t, 0, mockWriter.CommitCallCount(), "No commits should be made when StageModeCommitOnlyOnce is used") + case repository.StageModeCommitOnEach, repository.StageModeCommitAndPushOnEach: + if tt.wantError == nil || strings.Contains(tt.wantError.Error(), "push failed") { + require.Equal(t, 1, mockWriter.CommitCallCount(), "One commit should be made when using commit modes (even if push fails)") + } else if tt.wantError != nil && strings.Contains(tt.wantError.Error(), "commit") { + // Commit failed, so it should have been attempted but failed + require.Equal(t, 1, mockWriter.CommitCallCount(), "Commit should be attempted even if it fails") + } else if tt.wantError != nil { + require.Equal(t, 0, mockWriter.CommitCallCount(), "No commits should be made when error occurs before commit") + } + default: + // Default behavior (backward compatibility) + if tt.wantError == nil || strings.Contains(tt.wantError.Error(), "push failed") { + require.Equal(t, 1, mockWriter.CommitCallCount(), "One commit should be made with default mode (even if push fails)") + } else if tt.wantError != nil { + require.Equal(t, 0, mockWriter.CommitCallCount(), "No commits should be made when error occurs before commit") + } + } }) } } @@ -411,7 +481,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -430,7 +500,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.PushReturns(nil) }, opts: repository.StageOptions{ - PushOnWrites: true, + Mode: repository.StageModeCommitAndPushOnEach, }, path: "test.yaml", ref: "main", @@ -446,7 +516,7 @@ func TestStagedGitRepository_Write(t *testing.T) { // No setup needed as error occurs before writer calls }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "feature-branch", @@ -460,7 +530,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.BlobExistsReturns(false, errors.New("blob exists check failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -475,7 +545,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CreateBlobReturns(hash.Hash{}, errors.New("create failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -490,7 +560,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -506,7 +576,7 @@ func TestStagedGitRepository_Write(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -560,7 +630,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -577,7 +647,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.PushReturns(nil) }, opts: repository.StageOptions{ - PushOnWrites: true, + Mode: repository.StageModeCommitAndPushOnEach, }, path: "test.yaml", ref: "main", @@ -592,7 +662,7 @@ func TestStagedGitRepository_Update(t *testing.T) { // No setup needed as error occurs before writer calls }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "feature-branch", @@ -606,7 +676,7 @@ func TestStagedGitRepository_Update(t *testing.T) { // No setup needed as error occurs before writer calls }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "directory/", ref: "", @@ -620,7 +690,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.UpdateBlobReturns(hash.Hash{}, errors.New("update blob failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -635,7 +705,7 @@ func TestStagedGitRepository_Update(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -688,7 +758,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, nil) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -704,7 +774,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.PushReturns(nil) }, opts: repository.StageOptions{ - PushOnWrites: true, + Mode: repository.StageModeCommitAndPushOnEach, }, path: "testdir/", ref: "main", @@ -718,7 +788,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { // No setup needed as error occurs before writer calls }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "feature-branch", @@ -731,7 +801,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.DeleteBlobReturns(hash.Hash{}, errors.New("delete blob failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -745,7 +815,7 @@ func TestStagedGitRepository_Delete(t *testing.T) { mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) }, opts: repository.StageOptions{ - PushOnWrites: false, + Mode: repository.StageModeCommitOnEach, }, path: "test.yaml", ref: "", @@ -781,42 +851,96 @@ func TestStagedGitRepository_Delete(t *testing.T) { func TestStagedGitRepository_Push(t *testing.T) { tests := []struct { - name string - setupMock func(*mocks.FakeStagedWriter) - wantError error - expectCalls int + name string + opts repository.StageOptions + setupMock func(*mocks.FakeStagedWriter) + wantError error + expectPushCalls int + expectCommitCalls int }{ { - name: "succeeds with empty ref", + name: "succeeds with normal push", + opts: repository.StageOptions{}, setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(nil) }, - wantError: nil, - expectCalls: 1, - }, - { - name: "succeeds with matching ref", - setupMock: func(mockWriter *mocks.FakeStagedWriter) { - mockWriter.PushReturns(nil) - }, - wantError: nil, - expectCalls: 1, + wantError: nil, + expectPushCalls: 1, + expectCommitCalls: 0, }, { name: "succeeds with timeout", + opts: repository.StageOptions{ + Timeout: time.Second * 5, + }, setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(nil) }, - wantError: nil, - expectCalls: 1, + wantError: nil, + expectPushCalls: 1, + expectCommitCalls: 0, + }, + { + name: "succeeds with CommitOnlyOnce and default message", + opts: repository.StageOptions{ + Mode: repository.StageModeCommitOnlyOnce, + }, + setupMock: func(mockWriter *mocks.FakeStagedWriter) { + mockWriter.CommitReturns(&nanogit.Commit{}, nil) + mockWriter.PushReturns(nil) + }, + wantError: nil, + expectPushCalls: 1, + expectCommitCalls: 1, + }, + { + name: "succeeds with CommitOnlyOnce and custom message", + opts: repository.StageOptions{ + Mode: repository.StageModeCommitOnlyOnce, + CommitOnlyOnceMessage: "Custom commit message", + }, + setupMock: func(mockWriter *mocks.FakeStagedWriter) { + mockWriter.CommitReturns(&nanogit.Commit{}, nil) + mockWriter.PushReturns(nil) + }, + wantError: nil, + expectPushCalls: 1, + expectCommitCalls: 1, + }, + { + name: "fails with commit error when CommitOnlyOnce", + opts: repository.StageOptions{ + Mode: repository.StageModeCommitOnlyOnce, + }, + setupMock: func(mockWriter *mocks.FakeStagedWriter) { + mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) + }, + wantError: errors.New("commit changes: commit failed"), + expectPushCalls: 0, + expectCommitCalls: 1, }, { name: "fails with push error", + opts: repository.StageOptions{}, setupMock: func(mockWriter *mocks.FakeStagedWriter) { mockWriter.PushReturns(errors.New("push failed")) }, - wantError: errors.New("push failed"), - expectCalls: 1, + wantError: errors.New("push failed"), + expectPushCalls: 1, + expectCommitCalls: 0, + }, + { + name: "fails with push error after successful commit", + opts: repository.StageOptions{ + Mode: repository.StageModeCommitOnlyOnce, + }, + setupMock: func(mockWriter *mocks.FakeStagedWriter) { + mockWriter.CommitReturns(&nanogit.Commit{}, nil) + mockWriter.PushReturns(errors.New("push failed")) + }, + wantError: errors.New("push failed"), + expectPushCalls: 1, + expectCommitCalls: 1, }, } @@ -825,7 +949,7 @@ func TestStagedGitRepository_Push(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} tt.setupMock(mockWriter) - stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, repository.StageOptions{}) + stagedRepo := createTestStagedRepositoryWithWriter(mockWriter, tt.opts) err := stagedRepo.Push(context.Background()) @@ -835,7 +959,18 @@ func TestStagedGitRepository_Push(t *testing.T) { require.NoError(t, err) } - require.Equal(t, tt.expectCalls, mockWriter.PushCallCount()) + require.Equal(t, tt.expectPushCalls, mockWriter.PushCallCount()) + require.Equal(t, tt.expectCommitCalls, mockWriter.CommitCallCount()) + + // Verify commit message when CommitOnlyOnce is used + if tt.opts.Mode == repository.StageModeCommitOnlyOnce && tt.expectCommitCalls > 0 && tt.wantError == nil { + _, actualMessage, _, _ := mockWriter.CommitArgsForCall(0) + expectedMessage := tt.opts.CommitOnlyOnceMessage + if expectedMessage == "" { + expectedMessage = "Staged changes" + } + require.Equal(t, expectedMessage, actualMessage) + } }) } } diff --git a/pkg/registry/apis/provisioning/repository/github/repository_test.go b/pkg/registry/apis/provisioning/repository/github/repository_test.go index 2301156529e..960b80e42d0 100644 --- a/pkg/registry/apis/provisioning/repository/github/repository_test.go +++ b/pkg/registry/apis/provisioning/repository/github/repository_test.go @@ -985,8 +985,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo := git.NewMockGitRepository(t) mockStagedRepo := repository.NewMockStagedRepository(t) opts := repository.StageOptions{ - PushOnWrites: true, - Timeout: 10 * time.Second, + Mode: repository.StageModeCommitOnEach, + Timeout: 10 * time.Second, } mockGitRepo.On("Stage", ctx, opts).Return(mockStagedRepo, nil) diff --git a/pkg/registry/apis/provisioning/repository/staged.go b/pkg/registry/apis/provisioning/repository/staged.go index 6eb0aed357a..6649b085d5c 100644 --- a/pkg/registry/apis/provisioning/repository/staged.go +++ b/pkg/registry/apis/provisioning/repository/staged.go @@ -10,11 +10,25 @@ import ( "github.com/grafana/nanogit" ) +// StageMode defines the staging and commit behavior +type StageMode int + +const ( + // StageModeCommitOnEach commits each file operation individually (default) + StageModeCommitOnEach StageMode = iota + // StageModeCommitOnlyOnce stages all changes and commits them all at once on push + StageModeCommitOnlyOnce + // StageModeCommitAndPushOnEach commits and pushes each file operation individually + StageModeCommitAndPushOnEach +) + type StageOptions struct { - // Push on every write - PushOnWrites bool + // Mode defines the staging and commit behavior + Mode StageMode // Maximum time allowed for clone operation in seconds (0 means no limit) Timeout time.Duration + // Commit message to use when Mode is StageModeCommitOnlyOnce + CommitOnlyOnceMessage string } //go:generate mockery --name StageableRepository --structname MockStageableRepository --inpackage --filename stageable_repository_mock.go --with-expecter diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index 76fe0149441..4f1beaa5e67 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -2636,6 +2636,10 @@ "description": "The source folder (or empty) to export", "type": "string" }, + "message": { + "description": "Message to use when committing the changes in a single commit", + "type": "string" + }, "path": { "description": "Prefix in target file system", "type": "string" @@ -3156,6 +3160,10 @@ "history": { "description": "Preserve history (if possible)", "type": "boolean" + }, + "message": { + "description": "Message to use when committing the changes in a single commit", + "type": "string" } } }, diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts index 7122a2e268b..0ecc569f2cb 100644 --- a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts @@ -752,6 +752,8 @@ export type ObjectMeta = { export type MigrateJobOptions = { /** Preserve history (if possible) */ history?: boolean; + /** Message to use when committing the changes in a single commit */ + message?: string; }; export type PullRequestJobOptions = { /** The specific commit hash that triggered this notice */ @@ -772,6 +774,8 @@ export type ExportJobOptions = { branch?: string; /** The source folder (or empty) to export */ folder?: string; + /** Message to use when committing the changes in a single commit */ + message?: string; /** Prefix in target file system */ path?: string; }; From e280b949e3526f549928dcbf1f9e7f1794792671 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 24 Jul 2025 12:00:38 -0400 Subject: [PATCH 068/101] Alerting: Refactor remote Alertmanager and Mimir client (#107808) * deduplicate config preparation logic squashes config preparation in 3 places into a single method buildConfiguration * remove copying config from decrypt because we already use copy * move logic from decryptConfiguration to buildConfiguration * move logic from mergeExtraConfigs to buildConfiguration * load default config with buildConfiguration method and skip if fails --------- Co-authored-by: Santiago --- pkg/services/ngalert/remote/alertmanager.go | 173 +++++++----------- .../ngalert/remote/alertmanager_test.go | 5 +- 2 files changed, 69 insertions(+), 109 deletions(-) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index e1d1621f770..e577f7334e1 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -12,17 +12,16 @@ import ( "time" "github.com/go-openapi/strfmt" + alertingClusterPB "github.com/grafana/alerting/cluster/clusterpb" "github.com/grafana/alerting/definition" + alertingModels "github.com/grafana/alerting/models" + alertingNotify "github.com/grafana/alerting/notify" amalert "github.com/prometheus/alertmanager/api/v2/client/alert" amalertgroup "github.com/prometheus/alertmanager/api/v2/client/alertgroup" amgeneral "github.com/prometheus/alertmanager/api/v2/client/general" amsilence "github.com/prometheus/alertmanager/api/v2/client/silence" "github.com/prometheus/client_golang/prometheus" - alertingClusterPB "github.com/grafana/alerting/cluster/clusterpb" - alertingModels "github.com/grafana/alerting/models" - alertingNotify "github.com/grafana/alerting/notify" - "gopkg.in/yaml.v3" "github.com/grafana/grafana/pkg/infra/log" @@ -186,30 +185,12 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto return nil, err } - // Parse the default configuration into a postable config. - pCfg, err := notifier.Load([]byte(cfg.DefaultConfig)) - if err != nil { - return nil, err - } - - if err := autogenFn(ctx, logger, cfg.OrgID, &pCfg.AlertmanagerConfig, true); err != nil { - return nil, err - } - - rawCfg, err := json.Marshal(pCfg) - if err != nil { - return nil, err - } - - // Initialize LastReadinessCheck so it's present even if the check fails. - metrics.LastReadinessCheck.Set(0) - - return &Alertmanager{ + am := &Alertmanager{ amClient: amc, autogenFn: autogenFn, crypto: crypto, - defaultConfig: string(rawCfg), - defaultConfigHash: fmt.Sprintf("%x", md5.Sum(rawCfg)), + defaultConfig: cfg.DefaultConfig, + defaultConfigHash: "", // calculated below log: logger, metrics: metrics, mimirClient: mc, @@ -223,7 +204,31 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto // TODO: Remove once it can be sent only in the 'smtp_config' field. smtpFrom: cfg.SmtpFrom, - }, nil + } + + // Parse the default configuration once and remember its hash so we can compare it later. + // Known edge case: assigning a default contact point to a rule and setting route overrides + // (grouping, group timing, time intervals etc) changes the autogenerated configuration. + // The `default` flag is sent to the remote Alertmanager for informational purposes, so we can tolerate this. + err = func() error { + defaultCfg, err := am.buildConfiguration(ctx, []byte(cfg.DefaultConfig)) + if err != nil { + return fmt.Errorf("unable to build default configuration: %w", err) + } + rawDefaultCfg, err := json.Marshal(defaultCfg) + if err != nil { + return fmt.Errorf("unable to marshal default configuration: %w", err) + } + am.defaultConfigHash = fmt.Sprintf("%x", md5.Sum(rawDefaultCfg)) + return nil + }() + if err != nil { + logger.Error("Unable to calculate hash of the default configuration. Remote Alertmanager will always get isDefault=false", "error", err) + } + // Initialize LastReadinessCheck so it's present even if the check fails. + metrics.LastReadinessCheck.Set(0) + + return am, nil } // ApplyConfig is called by the multi-org Alertmanager on startup and on every sync loop iteration (1m default). @@ -275,31 +280,14 @@ func (am *Alertmanager) checkReadiness(ctx context.Context) error { // CompareAndSendConfiguration checks whether a given configuration is being used by the remote Alertmanager. // If not, it sends the configuration to the remote Alertmanager. func (am *Alertmanager) CompareAndSendConfiguration(ctx context.Context, config *models.AlertConfiguration) error { - c, err := notifier.Load([]byte(config.AlertmanagerConfiguration)) + payload, err := am.buildConfiguration(ctx, []byte(config.AlertmanagerConfiguration)) if err != nil { - return err - } - - // Add auto-generated routes and decrypt before comparing. - if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, true); err != nil { - return err - } - - decryptedCfg, err := am.decryptConfiguration(ctx, c) - if err != nil { - return err - } - - // Decrypt and merge extra configs - payload, err := am.mergeExtraConfigs(ctx, decryptedCfg) - if err != nil { - return fmt.Errorf("unable to merge extra configurations: %w", err) + return fmt.Errorf("unable to build configuration: %w", err) } rawPayload, err := json.Marshal(payload) if err != nil { return fmt.Errorf("unable to marshal decrypted configuration: %w", err) } - configHash := fmt.Sprintf("%x", md5.Sum(rawPayload)) // Send the configuration only if we need to. @@ -314,30 +302,6 @@ func (am *Alertmanager) isDefaultConfiguration(configHash string) bool { return configHash == am.defaultConfigHash } -// decryptConfiguration creates a copy of the configuration, decrypts it, and returns the decrypted configuration alongside its hash. -// Should not be used outside of this package and the specific use case of decrypting the configuration before sending -// it to the remote Alertmanager. -func (am *Alertmanager) decryptConfiguration(ctx context.Context, cfg *apimodels.PostableUserConfig) (*apimodels.PostableUserConfig, error) { - // Create a copy of the configuration to avoid modifying the original - cfgCopy := &apimodels.PostableUserConfig{} - rawCfg, err := json.Marshal(cfg) - if err != nil { - return nil, fmt.Errorf("unable to marshal original configuration: %w", err) - } - if err := json.Unmarshal(rawCfg, cfgCopy); err != nil { - return nil, fmt.Errorf("unable to unmarshal original configuration: %w", err) - } - - // Decrypt the receivers in the configuration. - decryptedReceivers, err := legacy_storage.DecryptedReceivers(cfgCopy.AlertmanagerConfig.Receivers, decrypter(ctx, am.crypto)) - if err != nil { - return nil, fmt.Errorf("unable to decrypt receivers: %w", err) - } - cfgCopy.AlertmanagerConfig.Receivers = decryptedReceivers - - return cfgCopy, nil -} - func decrypter(ctx context.Context, crypto Crypto) models.DecryptFn { return func(value string) (string, error) { decoded, err := base64.StdEncoding.DecodeString(value) @@ -352,31 +316,42 @@ func decrypter(ctx context.Context, crypto Crypto) models.DecryptFn { } } -// mergeExtraConfigs decrypts and applies merged configuration if extra configs exist. -func (am *Alertmanager) mergeExtraConfigs(ctx context.Context, config *apimodels.PostableUserConfig) (remoteClient.GrafanaAlertmanagerConfig, error) { - if len(config.ExtraConfigs) == 0 { - return remoteClient.GrafanaAlertmanagerConfig{ - TemplateFiles: config.TemplateFiles, - AlertmanagerConfig: config.AlertmanagerConfig, - Templates: nil, - }, nil +// buildConfiguration takes a raw Alertmanager configuration and returns a config that the remote Alertmanager can use. +// It parses the initial configuration, adds auto-generated routes, decrypts receivers, and merges the extra configs. +func (am *Alertmanager) buildConfiguration(ctx context.Context, raw []byte) (remoteClient.GrafanaAlertmanagerConfig, error) { + c, err := notifier.Load(raw) + if err != nil { + return remoteClient.GrafanaAlertmanagerConfig{}, err } - if err := am.crypto.DecryptExtraConfigs(ctx, config); err != nil { + // Add auto-generated routes and decrypt before comparing. + if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, true); err != nil { + return remoteClient.GrafanaAlertmanagerConfig{}, err + } + + // Decrypt the receivers in the configuration. + decryptedReceivers, err := legacy_storage.DecryptedReceivers(c.AlertmanagerConfig.Receivers, decrypter(ctx, am.crypto)) + if err != nil { + return remoteClient.GrafanaAlertmanagerConfig{}, fmt.Errorf("unable to decrypt receivers: %w", err) + } + c.AlertmanagerConfig.Receivers = decryptedReceivers + + if err := am.crypto.DecryptExtraConfigs(ctx, c); err != nil { return remoteClient.GrafanaAlertmanagerConfig{}, fmt.Errorf("unable to decrypt extra configs: %w", err) } - mergeResult, err := config.GetMergedAlertmanagerConfig() + mergeResult, err := c.GetMergedAlertmanagerConfig() if err != nil { return remoteClient.GrafanaAlertmanagerConfig{}, fmt.Errorf("unable to get merged Alertmanager configuration: %w", err) } - if logctx := mergeResult.LogContext(); len(logctx) > 0 { - am.log.Debug("Configurations merged successfully but some resources were renamed", logctx...) + + var templates []definition.PostableApiTemplate + if len(c.ExtraConfigs) > 0 && len(c.ExtraConfigs[0].TemplateFiles) > 0 { + templates = definition.TemplatesMapToPostableAPITemplates(c.ExtraConfigs[0].TemplateFiles, definition.MimirTemplateKind) } - templates := definition.TemplatesMapToPostableAPITemplates(config.ExtraConfigs[0].TemplateFiles, definition.MimirTemplateKind) + return remoteClient.GrafanaAlertmanagerConfig{ - // TODO keep sending Grafana templates as a map to not break old Mimir - TemplateFiles: config.TemplateFiles, + TemplateFiles: c.TemplateFiles, AlertmanagerConfig: mergeResult.Config, Templates: templates, }, nil @@ -456,19 +431,17 @@ func (am *Alertmanager) SendState(ctx context.Context) error { // SaveAndApplyConfig decrypts and sends a configuration to the remote Alertmanager. func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.PostableUserConfig) error { - // Add auto-generated routes and decrypt before sending. - if err := am.autogenFn(ctx, am.log, am.orgID, &cfg.AlertmanagerConfig, false); err != nil { - return err - } - decryptedCfg, err := am.decryptConfiguration(ctx, cfg) + // Copy the configuration by marshalling to avoid any mutations to the provided configuration. + rawCopy, err := json.Marshal(cfg) if err != nil { return err } - payload, err := am.mergeExtraConfigs(ctx, decryptedCfg) + payload, err := am.buildConfiguration(ctx, rawCopy) if err != nil { - return fmt.Errorf("unable to merge extra configurations: %w", err) + return fmt.Errorf("unable to build configuration: %w", err) } + rawCfg, err := json.Marshal(payload) if err != nil { return err @@ -480,24 +453,12 @@ func (am *Alertmanager) SaveAndApplyConfig(ctx context.Context, cfg *apimodels.P // SaveAndApplyDefaultConfig sends the default Grafana Alertmanager configuration to the remote Alertmanager. func (am *Alertmanager) SaveAndApplyDefaultConfig(ctx context.Context) error { - c, err := notifier.Load([]byte(am.defaultConfig)) + am.log.Debug("Sending default configuration to a remote Alertmanager", "url", am.url) + payload, err := am.buildConfiguration(ctx, []byte(am.defaultConfig)) if err != nil { - return fmt.Errorf("unable to parse the default configuration: %w", err) + return fmt.Errorf("unable to build default configuration: %w", err) } - // Add auto-generated routes and decrypt before sending. - if err := am.autogenFn(ctx, am.log, am.orgID, &c.AlertmanagerConfig, true); err != nil { - return err - } - decryptedCfg, err := am.decryptConfiguration(ctx, c) - if err != nil { - return err - } - - payload := remoteClient.GrafanaAlertmanagerConfig{ - TemplateFiles: c.TemplateFiles, - AlertmanagerConfig: decryptedCfg.AlertmanagerConfig, - } rawCfg, err := json.Marshal(payload) if err != nil { return err diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index c8d379089cd..bf1d9f0bfbd 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -411,10 +411,9 @@ func TestIntegrationApplyConfig(t *testing.T) { require.Equal(t, 4, configSyncs) require.Equal(t, am.smtp, configSent.SmtpConfig) - // Failing to add the auto-generated routes should result in an error. + // Failing to add the auto-generated routes should not result in an error. _, err = NewAlertmanager(context.Background(), cfg, fstore, notifier.NewCrypto(secretsService, nil, log.NewNopLogger()), errAutogenFn, m, tracing.InitializeTracerForTest()) - require.ErrorIs(t, err, errTest) - require.Equal(t, 4, configSyncs) + require.NoError(t, err, errTest) } func TestCompareAndSendConfiguration(t *testing.T) { From 716ae2a2661c6df15086de1698ec2171cf3055bf Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 24 Jul 2025 17:08:42 +0100 Subject: [PATCH 069/101] re-yarn lock (#108615) --- yarn.lock | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index e0dc68b1692..6dbb29257ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17532,20 +17532,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0, form-data@npm:~4.0.0": - version: 4.0.4 - resolution: "form-data@npm:4.0.4" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.12" - checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb - languageName: node - linkType: hard - -"form-data@npm:^4.0.4": +"form-data@npm:^4.0.0, form-data@npm:^4.0.4, form-data@npm:~4.0.0": version: 4.0.4 resolution: "form-data@npm:4.0.4" dependencies: From 6b259435eee499263323cf0a10c717c7057547ef Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Thu, 24 Jul 2025 12:31:55 -0400 Subject: [PATCH 070/101] PreviewBannerViewPR: Display different provider in preview banner (#108490) * PreviewBannerViewPR: display different provider in preview banner, update copy --- .../BulkDeleteProvisionedResource.tsx | 9 +- .../DeleteProvisionedFolderForm.test.tsx | 8 +- .../DeleteProvisionedFolderForm.tsx | 8 +- .../components/NewProvisionedFolderForm.tsx | 12 ++ .../provisioned/DashboardPreviewBanner.tsx | 8 +- .../provisioned/PreviewBannerViewPR.test.tsx | 116 ++++++++++++++---- .../provisioned/PreviewBannerViewPR.tsx | 60 ++++++--- .../SaveProvisionedDashboardForm.tsx | 10 +- .../DeleteProvisionedDashboardForm.tsx | 12 +- .../MoveProvisionedDashboardForm.test.tsx | 7 ++ .../settings/MoveProvisionedDashboardForm.tsx | 9 +- .../dashboard-scene/settings/utils.ts | 27 ++++ .../app/features/provisioning/Wizard/types.ts | 10 ++ .../provisioning/hooks/usePullRequestParam.ts | 2 + public/locales/en-US/grafana.json | 16 +-- 15 files changed, 250 insertions(+), 64 deletions(-) diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx index 77730c5320c..d5fd35ff1bd 100644 --- a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx @@ -15,6 +15,7 @@ import { AnnoKeySourcePath } from 'app/features/apiserver/types'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields'; import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults'; import { generateTimestamp } from 'app/features/dashboard-scene/saving/provisioned/utils/timestamp'; +import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils'; import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView'; import { WorkflowOption } from 'app/features/provisioning/types'; import { useSelector } from 'app/types/store'; @@ -89,7 +90,13 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions if (workflow === 'branch') { onDismiss?.(); if (successState.repoUrl) { - navigate({ search: `?repo_url=${encodeURIComponent(successState.repoUrl)}` }); + const url = buildResourceBranchRedirectUrl({ + paramName: 'repo_url', + paramValue: successState.repoUrl, + repoType: repository.type, + }); + + navigate(url); return; } window.location.reload(); diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx index 7bd6c28587a..00687677e94 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx @@ -297,9 +297,11 @@ describe('DeleteProvisionedFolderForm', () => { const { mockNavigate } = setup({}, { ...defaultHookData, initialValues: branchFormData }, successState); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith( - '/dashboards?new_pull_request_url=https://github.com/test/repo/pull/new' - ); + const expectedParams = new URLSearchParams(); + expectedParams.set('new_pull_request_url', 'https://github.com/test/repo/pull/new'); + const expectedUrl = `/dashboards?${expectedParams.toString()}`; + + expect(mockNavigate).toHaveBeenCalledWith(expectedUrl); }); }); }); diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx index ccf09a2f7c7..70638377a3a 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.tsx @@ -11,6 +11,7 @@ import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/ap import { AnnoKeySourcePath } from 'app/features/apiserver/types'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields'; import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared'; +import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils'; import { FolderDTO } from 'app/types/folders'; import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData'; @@ -61,7 +62,12 @@ function FormContent({ initialValues, parentFolder, repository, workflowOptions, if (request.isSuccess && repository) { const prUrl = request.data?.urls?.newPullRequestURL; if (workflow === 'branch' && prUrl) { - navigate(`/dashboards?new_pull_request_url=${prUrl}`); + const url = buildResourceBranchRedirectUrl({ + paramName: 'new_pull_request_url', + paramValue: prUrl, + repoType: request.data?.repository?.type, + }); + navigate(url); return; } diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx index 9e35078c946..a453490e07c 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx @@ -12,6 +12,7 @@ import { RepositoryView, useCreateRepositoryFilesWithPathMutation } from 'app/ap import { AnnoKeySourcePath, Resource } from 'app/features/apiserver/types'; import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields'; import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared'; +import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils'; import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; import { FolderDTO } from 'app/types/folders'; @@ -61,6 +62,17 @@ function FormContent({ initialValues, repository, workflowOptions, folder, onDis ], }); + const prUrl = request.data?.urls?.newPullRequestURL; + if (workflow === 'branch' && prUrl) { + const url = buildResourceBranchRedirectUrl({ + paramName: 'new_pull_request_url', + paramValue: prUrl, + repoType: request.data?.repository?.type, + }); + navigate(url); + return; + } + // TODO: Update when the upsert type is fixed // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const folder = request.data.resource?.upsert as Resource; diff --git a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx index f44552cbd69..2c23481b5eb 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/DashboardPreviewBanner.tsx @@ -48,10 +48,10 @@ function DashboardPreviewBannerContent({ queryParams, slug, path }: DashboardPre return ; } - // Check if this is a GitHub link - const githubURL = file.data?.urls?.newPullRequestURL ?? file.data?.urls?.compareURL; - if (githubURL) { - return ; + // Check if this is a repo link + const repoUrl = file.data?.urls?.newPullRequestURL ?? file.data?.urls?.compareURL; + if (repoUrl) { + return ; } return ( diff --git a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx index 94964f8004a..b742368dbce 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.test.tsx @@ -2,8 +2,10 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { textUtil } from '@grafana/data'; +import { RepoType } from 'app/features/provisioning/Wizard/types'; +import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; -import { PreviewBannerViewPR } from './PreviewBannerViewPR'; +import { isValidRepoType, PreviewBannerViewPR } from './PreviewBannerViewPR'; jest.mock('@grafana/data', () => ({ ...jest.requireActual('@grafana/data'), @@ -12,23 +14,33 @@ jest.mock('@grafana/data', () => ({ }, })); -jest.mock('@grafana/i18n', () => ({ - t: jest.fn((key: string, defaultValue: string) => defaultValue), - Trans: ({ children }: { children: React.ReactNode }) => children, +jest.mock('app/features/provisioning/hooks/usePullRequestParam', () => ({ + usePullRequestParam: jest.fn(), })); const mockTextUtil = jest.mocked(textUtil); -function setup(props: { prParam: string; isFolder?: boolean; isNewPr?: boolean } = { prParam: 'test-url' }) { - const defaultProps = { - isFolder: false, - isNewPr: false, - ...props, +const mockUsePullRequestParam = jest.mocked(usePullRequestParam); + +function setup( + options: { prParam: string; isNewPr?: boolean; repoType?: RepoType } = { prParam: 'test-url', repoType: 'github' } +) { + const componentProps = { + prParam: options.prParam, + isNewPr: options.isNewPr || false, }; - const renderResult = render(); + // Mock the hook BEFORE rendering the component + mockUsePullRequestParam.mockReturnValue({ + prURL: undefined, + newPrURL: undefined, + repoURL: undefined, + repoType: options.repoType || 'github', + }); - return { renderResult, props: defaultProps }; + const renderResult = render(); + + return { renderResult, props: componentProps }; } describe('PreviewBannerViewPR', () => { @@ -57,55 +69,63 @@ describe('PreviewBannerViewPR', () => { describe('Dashboard scenarios', () => { it('should render correct text for new PR dashboard', () => { - setup({ prParam: 'test-url', isFolder: false, isNewPr: true }); + setup({ prParam: 'test-url', isNewPr: true }); expect(screen.getByRole('status')).toBeInTheDocument(); expect(screen.getByText('A new resource has been created in a branch in GitHub.')).toBeInTheDocument(); }); it('should render correct text for existing PR dashboard', () => { - setup({ prParam: 'test-url', isFolder: false, isNewPr: false }); + setup({ prParam: 'test-url', isNewPr: false }); expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('This resource is loaded from a pull request in GitHub.')).toBeInTheDocument(); + expect( + screen.getByText( + 'The rest of Grafana users in your organization will still see the current version saved to configured default branch until this branch is merged' + ) + ).toBeInTheDocument(); }); it('should render correct button text for new PR dashboard', () => { - setup({ prParam: 'test-url', isFolder: false, isNewPr: true }); + setup({ prParam: 'test-url', isNewPr: true }); expect(screen.getByText('Open pull request in GitHub')).toBeInTheDocument(); }); it('should render correct button text for existing PR dashboard', () => { - setup({ prParam: 'test-url', isFolder: false, isNewPr: false }); + setup({ prParam: 'test-url', isNewPr: false }); expect(screen.getByText('View pull request in GitHub')).toBeInTheDocument(); }); }); - describe('Folder scenarios', () => { - it('should render correct text for new PR folder', () => { - setup({ prParam: 'test-url', isFolder: true, isNewPr: true }); + describe('Additional scenarios', () => { + it('should render correct text for new PR resource', () => { + setup({ prParam: 'test-url', isNewPr: true }); expect(screen.getByRole('status')).toBeInTheDocument(); expect(screen.getByText('A new resource has been created in a branch in GitHub.')).toBeInTheDocument(); }); - it('should render correct text for existing PR folder', () => { - setup({ prParam: 'test-url', isFolder: true, isNewPr: false }); + it('should render correct text for existing PR resource', () => { + setup({ prParam: 'test-url', isNewPr: false }); expect(screen.getByRole('status')).toBeInTheDocument(); - expect(screen.getByText('This resource is loaded from a pull request in GitHub.')).toBeInTheDocument(); + expect( + screen.getByText( + 'This resource is loaded from the branch you just created in GitHub and it is only visible to you' + ) + ).toBeInTheDocument(); }); - it('should render correct button text for new PR folder', () => { - setup({ prParam: 'test-url', isFolder: true, isNewPr: true }); + it('should render correct button text for new PR resource', () => { + setup({ prParam: 'test-url', isNewPr: true }); expect(screen.getByText('Open pull request in GitHub')).toBeInTheDocument(); }); - it('should render correct button text for existing PR folder', () => { - setup({ prParam: 'test-url', isFolder: true, isNewPr: false }); + it('should render correct button text for existing PR resource', () => { + setup({ prParam: 'test-url', isNewPr: false }); expect(screen.getByText('View pull request in GitHub')).toBeInTheDocument(); }); @@ -113,7 +133,7 @@ describe('PreviewBannerViewPR', () => { describe('Button functionality', () => { it('should open URL in new tab when button is clicked', async () => { - const testUrl = 'https://github.com/test/repo/pull/123'; + const testUrl = 'https://GitHub.com/test/repo/pull/123'; setup({ prParam: testUrl }); const button = screen.getByRole('button', { name: /close alert/i }); @@ -122,4 +142,46 @@ describe('PreviewBannerViewPR', () => { expect(windowOpenSpy).toHaveBeenCalledWith(testUrl, '_blank'); }); }); + + describe('Different repository types', () => { + it('should handle GitLab repository type', () => { + setup({ prParam: 'test-url', isNewPr: false, repoType: 'gitlab' }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect( + screen.getByText( + 'This resource is loaded from the branch you just created in GitLab and it is only visible to you' + ) + ).toBeInTheDocument(); + }); + + it('should handle Bitbucket repository type', () => { + setup({ prParam: 'test-url', isNewPr: false, repoType: 'bitbucket' }); + + expect(screen.getByRole('status')).toBeInTheDocument(); + expect( + screen.getByText( + 'This resource is loaded from the branch you just created in Bitbucket and it is only visible to you' + ) + ).toBeInTheDocument(); + }); + }); +}); + +describe('isValidRepoType', () => { + it('should return true for valid repo types', () => { + expect(isValidRepoType('github')).toBe(true); + expect(isValidRepoType('gitlab')).toBe(true); + expect(isValidRepoType('bitbucket')).toBe(true); + expect(isValidRepoType('git')).toBe(true); + }); + + it('should return false for invalid repo types', () => { + expect(isValidRepoType('unknown')).toBe(false); + expect(isValidRepoType('apple')).toBe(false); + expect(isValidRepoType('')).toBe(false); + expect(isValidRepoType(undefined)).toBe(false); + // @ts-expect-error testing invalid type + expect(isValidRepoType(null)).toBe(false); + }); }); diff --git a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx index 54eef1b265b..da711f084ca 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/PreviewBannerViewPR.tsx @@ -1,6 +1,8 @@ import { textUtil } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { Alert, Icon, Stack } from '@grafana/ui'; +import { RepoTypeDisplay, RepoType } from 'app/features/provisioning/Wizard/types'; +import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam'; import { commonAlertProps } from './DashboardPreviewBanner'; @@ -14,17 +16,27 @@ interface Props { } /** - * @description This component is used to display a banner when a provisioned dashboard/folder is created or loaded from a new branch in Github. + * @description This component is used to display a banner when a provisioned dashboard/folder is created or loaded from a new branch in repo. */ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }: Props) { + const { repoType } = usePullRequestParam(); + + const capitalizedRepoType = isValidRepoType(repoType) ? RepoTypeDisplay[repoType] : 'repository'; + const titleText = isNewPr ? t( - 'provisioned-resource-preview-banner.title-created-branch-git-hub', - 'A new resource has been created in a branch in GitHub.' + 'provisioned-resource-preview-banner.title-created-branch-in-repo', + 'A new resource has been created in a branch in {{repoType}}.', + { + repoType: capitalizedRepoType, + } ) : t( - 'provisioned-resource-preview-banner.title-loaded-pull-request-git-hub', - 'This resource is loaded from a pull request in GitHub.' + 'provisioned-resource-preview-banner.title-loaded-pull-request-in-repo', + 'This resource is loaded from the branch you just created in {{repoType}} and it is only visible to you', + { + repoType: capitalizedRepoType, + } ); if (behindBranch) { @@ -33,18 +45,26 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }: {...commonAlertProps} buttonContent={ - {t('provisioned-resource-preview-banner.preview-banner.open-git-hub', 'Open in Github')} + {t('provisioned-resource-preview-banner.preview-banner.open-in-repo-button', 'Open in {{repoType}}', { + repoType: capitalizedRepoType, + })} } title={t( - 'provisioned-resource-preview-banner.preview-banner.behind-branch', - 'This resource is behind the branch in GitHub.' + 'provisioned-resource-preview-banner.preview-banner.behind-branch-text', + 'This resource is behind the branch in {{repoType}}.', + { + repoType: capitalizedRepoType, + } )} onRemove={repoUrl ? () => window.open(textUtil.sanitizeUrl(repoUrl), '_blank') : undefined} > - - View it in GitHub to see the latest changes. + + View it in {{ repoType }} to see the latest changes. ); @@ -58,12 +78,14 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }: {isNewPr ? t( - 'provisioned-resource-preview-banner.preview-banner.open-pull-request-in-git-hub', - 'Open pull request in GitHub' + 'provisioned-resource-preview-banner.preview-banner.open-pull-request-in-repo', + 'Open pull request in {{repoType}}', + { repoType: capitalizedRepoType } ) : t( - 'provisioned-resource-preview-banner.preview-banner.view-pull-request-in-git-hub', - 'View pull request in GitHub' + 'provisioned-resource-preview-banner.preview-banner.view-pull-request-in-repo', + 'View pull request in {{repoType}}', + { repoType: capitalizedRepoType } )} @@ -71,8 +93,16 @@ export function PreviewBannerViewPR({ prParam, isNewPr, behindBranch, repoUrl }: onRemove={prParam ? () => window.open(textUtil.sanitizeUrl(prParam), '_blank') : undefined} > - The value is not yet saved in the Grafana database + The rest of Grafana users in your organization will still see the current version saved to configured default + branch until this branch is merged ); } + +export function isValidRepoType(repoType: string | undefined): repoType is RepoType { + if (typeof repoType !== 'string') { + return false; + } + return repoType in RepoTypeDisplay; +} diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx index e20e4c811b0..4387af82439 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx @@ -16,6 +16,7 @@ import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile'; import { ResourceEditFormSharedFields } from '../../components/Provisioned/ResourceEditFormSharedFields'; +import { buildResourceBranchRedirectUrl } from '../../settings/utils'; import { getDashboardUrl } from '../../utils/getDashboardUrl'; import { useProvisionedRequestHandler } from '../../utils/useProvisionedRequestHandler'; import { SaveDashboardFormCommonOptions } from '../SaveDashboardForm'; @@ -92,7 +93,14 @@ export function SaveProvisionedDashboardForm({ const onBranchSuccess = (ref: string, path: string) => { panelEditor?.onDiscard(); drawer.onClose(); - navigate(`${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}?ref=${ref}`); + + const url = buildResourceBranchRedirectUrl({ + baseUrl: `${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}`, + paramName: 'ref', + paramValue: ref, + repoType: request.data?.repository?.type, + }); + navigate(url); }; useProvisionedRequestHandler({ diff --git a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx index d7b62a5cf22..53bb342cd21 100644 --- a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx @@ -13,6 +13,8 @@ import { ProvisionedDashboardFormData } from '../saving/shared'; import { DashboardScene } from '../scene/DashboardScene'; import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler'; +import { buildResourceBranchRedirectUrl } from './utils'; + export interface Props { dashboard: DashboardScene; defaultValues: ProvisionedDashboardFormData; @@ -82,9 +84,13 @@ export function DeleteProvisionedDashboardForm({ const onBranchSuccess = (path: string, urls?: Record) => { panelEditor?.onDiscard(); onDismiss(); - navigate( - `${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}?pull_request_url=${urls?.newPullRequestURL}` - ); + const url = buildResourceBranchRedirectUrl({ + baseUrl: `${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}`, + paramName: 'pull_request_url', + paramValue: urls?.newPullRequestURL, + repoType: request.data?.repository?.type, + }); + navigate(url); }; useProvisionedRequestHandler({ diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx index 2c52d7ddb9a..30600a0e6df 100644 --- a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.test.tsx @@ -27,6 +27,13 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => ({ useGetRepositoryFilesWithPathQuery: jest.fn(), useCreateRepositoryFilesWithPathMutation: jest.fn(), useDeleteRepositoryFilesWithPathMutation: jest.fn(), + provisioningAPIv0alpha1: { + endpoints: { + listRepository: { + select: jest.fn(() => () => ({ data: { items: [] } })), + }, + }, + }, })); jest.mock('app/api/clients/folder/v1beta1', () => ({ diff --git a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx index 94951c10f46..a9e62edc3db 100644 --- a/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/settings/MoveProvisionedDashboardForm.tsx @@ -20,6 +20,8 @@ import { ProvisionedDashboardFormData } from '../saving/shared'; import { DashboardScene } from '../scene/DashboardScene'; import { useProvisionedRequestHandler } from '../utils/useProvisionedRequestHandler'; +import { buildResourceBranchRedirectUrl } from './utils'; + export interface Props { dashboard: DashboardScene; defaultValues: ProvisionedDashboardFormData; @@ -144,7 +146,12 @@ export function MoveProvisionedDashboardForm({ const onBranchSuccess = () => { panelEditor?.onDiscard(); - navigate(`/dashboards?new_pull_request_url=${createRequest.data?.urls?.newPullRequestURL}`); + const url = buildResourceBranchRedirectUrl({ + paramName: 'new_pull_request_url', + paramValue: createRequest?.data?.urls?.newPullRequestURL, + repoType: createRequest?.data?.repository?.type, + }); + navigate(url); }; useProvisionedRequestHandler({ diff --git a/public/app/features/dashboard-scene/settings/utils.ts b/public/app/features/dashboard-scene/settings/utils.ts index 88011f08b00..741830b8ec5 100644 --- a/public/app/features/dashboard-scene/settings/utils.ts +++ b/public/app/features/dashboard-scene/settings/utils.ts @@ -111,3 +111,30 @@ export function createDashboardEditViewFor(editview: string): DashboardEditView return new GeneralSettingsEditView({}); } } + +export type ResourceBranchUrlOptions = { + baseUrl?: string; + paramName?: string; + paramValue?: string; + repoType?: string; +}; + +export function buildResourceBranchRedirectUrl({ + baseUrl = '/dashboards', + paramName, + paramValue, + repoType, +}: ResourceBranchUrlOptions): string { + const params = new URLSearchParams(); + + if (paramName && paramValue) { + params.set(paramName, paramValue); + } + + if (repoType) { + params.set('repo_type', repoType); + } + + const queryString = params.toString(); + return queryString ? `${baseUrl}?${queryString}` : baseUrl; +} diff --git a/public/app/features/provisioning/Wizard/types.ts b/public/app/features/provisioning/Wizard/types.ts index fd9fcd00f1c..18c9fe36149 100644 --- a/public/app/features/provisioning/Wizard/types.ts +++ b/public/app/features/provisioning/Wizard/types.ts @@ -26,6 +26,16 @@ export interface ModeOption { subtitle: string; } +export type StepStatus = 'idle' | 'running' | 'error' | 'success'; + +export const RepoTypeDisplay: { [key in RepoType]: string } = { + github: 'GitHub', + gitlab: 'GitLab', + bitbucket: 'Bitbucket', + git: 'Git', + local: 'Local', +}; + export type StepStatusInfo = | { status: 'idle' | 'running' | 'success' } | { status: 'error'; error: string | ProvisioningErrorInfo } diff --git a/public/app/features/provisioning/hooks/usePullRequestParam.ts b/public/app/features/provisioning/hooks/usePullRequestParam.ts index 4934fff1ffc..81d10210503 100644 --- a/public/app/features/provisioning/hooks/usePullRequestParam.ts +++ b/public/app/features/provisioning/hooks/usePullRequestParam.ts @@ -6,10 +6,12 @@ export const usePullRequestParam = () => { const prParam = params.get('pull_request_url'); const newPrParam = params.get('new_pull_request_url'); const repoUrl = params.get('repo_url'); + const repoType = params.get('repo_type'); return { prURL: prParam ? textUtil.sanitizeUrl(prParam) : undefined, newPrURL: newPrParam ? textUtil.sanitizeUrl(newPrParam) : undefined, repoURL: repoUrl ? textUtil.sanitizeUrl(repoUrl) : undefined, + repoType: repoType ? textUtil.sanitizeUrl(repoType) : undefined, }; }; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 4de18dad330..20f007d6edf 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -10952,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "This resource is behind the branch in GitHub.", - "new-branch": "View it in GitHub to see the latest changes.", - "not-saved": "The value is not yet saved in the Grafana database", - "open-git-hub": "Open in Github", - "open-pull-request-in-git-hub": "Open pull request in GitHub", - "view-pull-request-in-git-hub": "View pull request in GitHub" + "behind-branch-text": "This resource is behind the branch in {{repoType}}.", + "not-saved": "The rest of Grafana users in your organization will still see the current version saved to configured default branch until this branch is merged", + "open-in-repo-button": "Open in {{repoType}}", + "open-pull-request-in-repo": "Open pull request in {{repoType}}", + "view-in-repo-button": "View it in {{repoType}} to see the latest changes.", + "view-pull-request-in-repo": "View pull request in {{repoType}}" }, - "title-created-branch-git-hub": "A new resource has been created in a branch in GitHub.", - "title-loaded-pull-request-git-hub": "This resource is loaded from a pull request in GitHub." + "title-created-branch-in-repo": "A new resource has been created in a branch in {{repoType}}.", + "title-loaded-pull-request-in-repo": "This resource is loaded from the branch you just created in {{repoType}} and it is only visible to you" }, "provisioning": { "banner": { From f515e5af7e7538180b6c236a76cdff82de08efee Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Thu, 24 Jul 2025 11:35:12 -0500 Subject: [PATCH 071/101] CODEOWNERS: Update integration test owners (#108621) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9d41fb8b7e..3329738f79c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -181,6 +181,9 @@ /pkg/tests/ @grafana/grafana-backend-services-squad /pkg/tests/apis/ @grafana/grafana-app-platform-squad /pkg/tests/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend +/pkg/tests/apis/features @grafana/grafana-backend-services-squad +/pkg/tests/apis/folder @grafana/grafana-search-and-storage +/pkg/tests/apis/iam @grafana/identity-access-team /pkg/tests/api/correlations/ @grafana/datapro /pkg/tsdb/grafanads/ @grafana/grafana-backend-group /pkg/tsdb/opentsdb/ @grafana/partner-datasources From 6eb400d1a7835ad11443b95d4466d6d08faae672 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:53:07 +0100 Subject: [PATCH 072/101] Update dependency eslint-plugin-jest-dom to v5.5.0 (#108606) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6dbb29257ab..6444733ff34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16308,8 +16308,8 @@ __metadata: linkType: hard "eslint-plugin-jest-dom@npm:^5.4.0": - version: 5.4.0 - resolution: "eslint-plugin-jest-dom@npm:5.4.0" + version: 5.5.0 + resolution: "eslint-plugin-jest-dom@npm:5.5.0" dependencies: "@babel/runtime": "npm:^7.16.3" requireindex: "npm:^1.2.0" @@ -16319,7 +16319,7 @@ __metadata: peerDependenciesMeta: "@testing-library/dom": optional: true - checksum: 10/b8b0b0249d066658a75723892bc6f52d6bcf03ff0a69fc5020548c49f740613a8f3acce647f8f04b292606d2bd0ab3372a695aa3d90b4efb19e71870bbddf637 + checksum: 10/73aaaa6117abbe3b197bc6b1e45839aaa9c2b4c86e7efbc4ff29f03318ec7f019a8e32652c06c61f06fdb22fb296c068a802a268e7b88aa6b71d3477d949b2c6 languageName: node linkType: hard From e9b9618fb01725344b24737e026160efdc92780b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Thu, 24 Jul 2025 18:53:33 +0200 Subject: [PATCH 073/101] Provisioning: Fix stage options for export (#108585) * Add ref option to stage options * Fix the issue with ref in export worker * Add unit tests for export stage options * Do not fail if ref is equal to the stage branch * Format code again * fix test --------- Co-authored-by: Stephanie Hingtgen --- .../apis/provisioning/jobs/export/worker.go | 4 +- .../provisioning/jobs/export/worker_test.go | 96 +++++++++++- .../provisioning/repository/git/staged.go | 36 ++++- .../repository/git/staged_test.go | 142 ++++++++++++++++-- .../apis/provisioning/repository/staged.go | 4 + 5 files changed, 260 insertions(+), 22 deletions(-) diff --git a/pkg/registry/apis/provisioning/jobs/export/worker.go b/pkg/registry/apis/provisioning/jobs/export/worker.go index 87405a881ef..13536a62aa5 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker.go @@ -62,9 +62,11 @@ func (r *ExportWorker) Process(ctx context.Context, repo repository.Repository, } cloneOptions := repository.StageOptions{ + Ref: options.Branch, + Timeout: 10 * time.Minute, + PushOnWrites: false, Mode: repository.StageModeCommitOnlyOnce, CommitOnlyOnceMessage: msg, - Timeout: 10 * time.Minute, } fn := func(repo repository.Repository, _ bool) error { diff --git a/pkg/registry/apis/provisioning/jobs/export/worker_test.go b/pkg/registry/apis/provisioning/jobs/export/worker_test.go index af419538a42..930b5f051a9 100644 --- a/pkg/registry/apis/provisioning/jobs/export/worker_test.go +++ b/pkg/registry/apis/provisioning/jobs/export/worker_test.go @@ -228,7 +228,9 @@ func TestExportWorker_ProcessStageOptions(t *testing.T) { job := v0alpha1.Job{ Spec: v0alpha1.JobSpec{ Action: v0alpha1.JobActionPush, - Push: &v0alpha1.ExportJobOptions{}, + Push: &v0alpha1.ExportJobOptions{ + Branch: "feature-branch", + }, }, } @@ -239,7 +241,8 @@ func TestExportWorker_ProcessStageOptions(t *testing.T) { Namespace: "test-namespace", }, Spec: v0alpha1.RepositorySpec{ - Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow}, + Type: v0alpha1.GitRepositoryType, + Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow, v0alpha1.BranchWorkflow}, }, }) @@ -258,9 +261,11 @@ func TestExportWorker_ProcessStageOptions(t *testing.T) { mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) mockStageFn := NewMockWrapWithStageFn(t) - // Verify clone and push options + // Verify all stage options including Ref (branch), Timeout, and PushOnWrites mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { - return opts.Timeout == 10*time.Minute && opts.Mode == repository.StageModeCommitOnlyOnce + return opts.Ref == "feature-branch" && + opts.Timeout == 10*time.Minute && + !opts.PushOnWrites && opts.Mode == repository.StageModeCommitOnlyOnce }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { return fn(repo, true) }) @@ -270,6 +275,89 @@ func TestExportWorker_ProcessStageOptions(t *testing.T) { require.NoError(t, err) } +func TestExportWorker_ProcessStageOptionsWithBranch(t *testing.T) { + tests := []struct { + name string + branch string + expectedRef string + workflows []v0alpha1.Workflow + repoType v0alpha1.RepositoryType + }{ + { + name: "branch specified", + branch: "develop", + expectedRef: "develop", + workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow, v0alpha1.BranchWorkflow}, + repoType: v0alpha1.GitRepositoryType, + }, + { + name: "empty branch", + branch: "", + expectedRef: "", + workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow}, + repoType: v0alpha1.LocalRepositoryType, + }, + { + name: "main branch", + branch: "main", + expectedRef: "main", + workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow, v0alpha1.BranchWorkflow}, + repoType: v0alpha1.GitRepositoryType, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + job := v0alpha1.Job{ + Spec: v0alpha1.JobSpec{ + Action: v0alpha1.JobActionPush, + Push: &v0alpha1.ExportJobOptions{ + Branch: tt.branch, + }, + }, + } + + mockRepo := repository.NewMockRepository(t) + mockRepo.On("Config").Return(&v0alpha1.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "test-namespace", + }, + Spec: v0alpha1.RepositorySpec{ + Type: tt.repoType, + Workflows: tt.workflows, + }, + }) + + mockProgress := jobs.NewMockJobProgressRecorder(t) + mockClients := resources.NewMockClientFactory(t) + mockResourceClients := resources.NewMockResourceClients(t) + mockClients.On("Clients", mock.Anything, "test-namespace").Return(mockResourceClients, nil) + + mockRepoResources := resources.NewMockRepositoryResourcesFactory(t) + mockRepoResourcesClient := resources.NewMockRepositoryResources(t) + mockRepoResources.On("Client", mock.Anything, mock.Anything).Return(mockRepoResourcesClient, nil) + + mockExportFn := NewMockExportFn(t) + mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) + + mockStageFn := NewMockWrapWithStageFn(t) + // Verify that the stage options contain the correct branch reference and other parameters + mockStageFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.StageOptions) bool { + return opts.Ref == tt.expectedRef && + opts.Timeout == 10*time.Minute && + !opts.PushOnWrites + }), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, stageOpts repository.StageOptions, fn func(repository.Repository, bool) error) error { + return fn(repo, true) + }) + + r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockStageFn.Execute) + err := r.Process(context.Background(), mockRepo, job, mockProgress) + require.NoError(t, err) + }) + } +} + func TestExportWorker_ProcessExportFnError(t *testing.T) { job := v0alpha1.Job{ Spec: v0alpha1.JobSpec{ diff --git a/pkg/registry/apis/provisioning/repository/git/staged.go b/pkg/registry/apis/provisioning/repository/git/staged.go index e122bc8d33b..335193716b0 100644 --- a/pkg/registry/apis/provisioning/repository/git/staged.go +++ b/pkg/registry/apis/provisioning/repository/git/staged.go @@ -26,7 +26,12 @@ func NewStagedGitRepository(ctx context.Context, repo *gitRepository, opts repos defer cancel() } - ref, err := repo.client.GetRef(ctx, "refs/heads/"+repo.gitConfig.Branch) + branch := opts.Ref + if branch == "" { + branch = repo.gitConfig.Branch + } + + ref, err := repo.client.GetRef(ctx, "refs/heads/"+branch) if err != nil { // TODO: opts.CreateIfNotExists doesn't make sense in the context of the staged repository // because we only support the branch that is passed in. @@ -47,8 +52,25 @@ func NewStagedGitRepository(ctx context.Context, repo *gitRepository, opts repos }, nil } +// isRefSupported checks if the given ref is supported for staged operations. +// It returns true if ref is empty, equals the git config branch, or equals the staged options ref. +func (r *stagedGitRepository) isRefSupported(ref string) bool { + if ref == "" { + return true + } + if ref == r.gitConfig.Branch { + return true + } + // Allow ref if it matches the staged options ref (the branch we're staging to) + stagingBranch := r.opts.Ref + if stagingBranch == "" { + stagingBranch = r.gitConfig.Branch + } + return ref == stagingBranch +} + func (r *stagedGitRepository) Read(ctx context.Context, path, ref string) (*repository.FileInfo, error) { - if ref != "" && ref != r.gitConfig.Branch { + if !r.isRefSupported(ref) { return nil, errors.New("ref is not supported for staged repository") } @@ -58,7 +80,7 @@ func (r *stagedGitRepository) Read(ctx context.Context, path, ref string) (*repo } func (r *stagedGitRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { - if ref != "" && ref != r.gitConfig.Branch { + if !r.isRefSupported(ref) { return nil, errors.New("ref is not supported for staged repository") } @@ -89,7 +111,7 @@ func (r *stagedGitRepository) handleCommitAndPush(ctx context.Context, message s } func (r *stagedGitRepository) Create(ctx context.Context, path, ref string, data []byte, message string) error { - if ref != "" && ref != r.gitConfig.Branch { + if !r.isRefSupported(ref) { return errors.New("ref is not supported for staged repository") } @@ -108,7 +130,7 @@ func (r *stagedGitRepository) blobExists(ctx context.Context, path string) (bool } func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data []byte, message string) error { - if ref != "" && ref != r.gitConfig.Branch { + if !r.isRefSupported(ref) { return errors.New("ref is not supported for staged repository") } @@ -131,7 +153,7 @@ func (r *stagedGitRepository) Write(ctx context.Context, path, ref string, data } func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data []byte, message string) error { - if ref != "" && ref != r.gitConfig.Branch { + if !r.isRefSupported(ref) { return errors.New("ref is not supported for staged repository") } @@ -147,7 +169,7 @@ func (r *stagedGitRepository) Update(ctx context.Context, path, ref string, data } func (r *stagedGitRepository) Delete(ctx context.Context, path, ref, message string) error { - if ref != "" && ref != r.gitConfig.Branch { + if !r.isRefSupported(ref) { return errors.New("ref is not supported for staged repository") } diff --git a/pkg/registry/apis/provisioning/repository/git/staged_test.go b/pkg/registry/apis/provisioning/repository/git/staged_test.go index 73604e72fa3..0a614b94c57 100644 --- a/pkg/registry/apis/provisioning/repository/git/staged_test.go +++ b/pkg/registry/apis/provisioning/repository/git/staged_test.go @@ -17,10 +17,11 @@ import ( func TestNewStagedGitRepository(t *testing.T) { tests := []struct { - name string - setupMock func(*mocks.FakeClient) - opts repository.StageOptions - wantError error + name string + setupMock func(*mocks.FakeClient) + opts repository.StageOptions + wantError error + expectedRef string }{ { name: "succeeds with default options", @@ -32,11 +33,29 @@ func TestNewStagedGitRepository(t *testing.T) { mockWriter := &mocks.FakeStagedWriter{} mockClient.NewStagedWriterReturns(mockWriter, nil) }, + expectedRef: "refs/heads/main", opts: repository.StageOptions{ Mode: repository.StageModeCommitOnEach, }, wantError: nil, }, + { + name: "succeeds with custom ref option", + setupMock: func(mockClient *mocks.FakeClient) { + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/custom", + Hash: hash.Hash{1, 2, 3}, + }, nil) + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + }, + expectedRef: "refs/heads/custom", + opts: repository.StageOptions{ + Ref: "custom", + PushOnWrites: false, + }, + wantError: nil, + }, { name: "succeeds with BeforeFn", setupMock: func(mockClient *mocks.FakeClient) { @@ -50,7 +69,8 @@ func TestNewStagedGitRepository(t *testing.T) { opts: repository.StageOptions{ Mode: repository.StageModeCommitOnEach, }, - wantError: nil, + wantError: nil, + expectedRef: "refs/heads/main", }, { name: "succeeds with timeout", @@ -66,7 +86,8 @@ func TestNewStagedGitRepository(t *testing.T) { Mode: repository.StageModeCommitOnEach, Timeout: time.Second * 5, }, - wantError: nil, + expectedRef: "refs/heads/main", + wantError: nil, }, { name: "succeeds with CommitOnlyOnce option", @@ -82,7 +103,8 @@ func TestNewStagedGitRepository(t *testing.T) { Mode: repository.StageModeCommitOnlyOnce, CommitOnlyOnceMessage: "Custom commit message", }, - wantError: nil, + expectedRef: "refs/heads/main", + wantError: nil, }, { name: "succeeds with CommitAndPushOnEach option", @@ -97,7 +119,8 @@ func TestNewStagedGitRepository(t *testing.T) { opts: repository.StageOptions{ Mode: repository.StageModeCommitAndPushOnEach, }, - wantError: nil, + expectedRef: "refs/heads/main", + wantError: nil, }, { name: "fails with GetRef error", @@ -121,7 +144,8 @@ func TestNewStagedGitRepository(t *testing.T) { opts: repository.StageOptions{ Mode: repository.StageModeCommitOnEach, }, - wantError: errors.New("build staged writer: failed to create writer"), + wantError: errors.New("build staged writer: failed to create writer"), + expectedRef: "refs/heads/main", }, } @@ -156,6 +180,10 @@ func TestNewStagedGitRepository(t *testing.T) { require.Equal(t, tt.opts.Mode, actualOpts.Mode) require.Equal(t, tt.opts.Timeout, actualOpts.Timeout) require.Equal(t, tt.opts.CommitOnlyOnceMessage, actualOpts.CommitOnlyOnceMessage) + + // Verify the expected ref + _, ref := mockClient.GetRefArgsForCall(0) + require.Equal(t, tt.expectedRef, ref) } }) } @@ -207,6 +235,25 @@ func TestStagedGitRepository_Read(t *testing.T) { ref: "main", wantError: nil, }, + { + name: "succeeds with ref matching stage options", + setupMock: func(mockClient *mocks.FakeClient) { + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/feature", + Hash: hash.Hash{1, 2, 3}, + }, nil) + mockClient.GetCommitReturns(&nanogit.Commit{ + Tree: hash.Hash{4, 5, 6}, + }, nil) + mockClient.GetBlobByPathReturns(&nanogit.Blob{ + Content: []byte("file content"), + Hash: hash.Hash{7, 8, 9}, + }, nil) + }, + path: "test.yaml", + ref: "feature", + wantError: nil, + }, { name: "fails with unsupported ref", setupMock: func(_ *mocks.FakeClient) { @@ -223,7 +270,13 @@ func TestStagedGitRepository_Read(t *testing.T) { mockClient := &mocks.FakeClient{} tt.setupMock(mockClient) - stagedRepo := createTestStagedRepository(mockClient) + // Use stage options with ref "feature" for the specific test case + opts := repository.StageOptions{} + if tt.ref == "feature" { + opts.Ref = "feature" + } + + stagedRepo := createTestStagedRepositoryWithWriter(&mocks.FakeStagedWriter{}, opts, mockClient) fileInfo, err := stagedRepo.Read(context.Background(), tt.path, tt.ref) if tt.wantError != nil { require.EqualError(t, err, tt.wantError.Error()) @@ -986,6 +1039,75 @@ func TestStagedGitRepository_Remove(t *testing.T) { }) } +func TestStagedGitRepository_isRefSupported(t *testing.T) { + tests := []struct { + name string + stageOpts repository.StageOptions + gitBranch string + ref string + expected bool + }{ + { + name: "empty ref is supported", + stageOpts: repository.StageOptions{}, + gitBranch: "main", + ref: "", + expected: true, + }, + { + name: "ref matches git config branch", + stageOpts: repository.StageOptions{}, + gitBranch: "main", + ref: "main", + expected: true, + }, + { + name: "ref matches stage options ref", + stageOpts: repository.StageOptions{Ref: "feature"}, + gitBranch: "main", + ref: "feature", + expected: true, + }, + { + name: "ref matches stage options ref when empty defaults to git branch", + stageOpts: repository.StageOptions{Ref: ""}, + gitBranch: "main", + ref: "main", + expected: true, + }, + { + name: "unsupported ref", + stageOpts: repository.StageOptions{Ref: "feature"}, + gitBranch: "main", + ref: "other-branch", + expected: false, + }, + { + name: "unsupported ref with empty stage options", + stageOpts: repository.StageOptions{}, + gitBranch: "main", + ref: "feature", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stagedRepo := &stagedGitRepository{ + gitRepository: &gitRepository{ + gitConfig: RepositoryConfig{ + Branch: tt.gitBranch, + }, + }, + opts: tt.stageOpts, + } + + result := stagedRepo.isRefSupported(tt.ref) + require.Equal(t, tt.expected, result) + }) + } +} + // Helper functions for creating test instances func createTestStagedRepository(mockClient *mocks.FakeClient) *stagedGitRepository { diff --git a/pkg/registry/apis/provisioning/repository/staged.go b/pkg/registry/apis/provisioning/repository/staged.go index 6649b085d5c..bdcae0ab039 100644 --- a/pkg/registry/apis/provisioning/repository/staged.go +++ b/pkg/registry/apis/provisioning/repository/staged.go @@ -23,6 +23,10 @@ const ( ) type StageOptions struct { + // Ref custom ref + Ref string + // Push on every write + PushOnWrites bool // Mode defines the staging and commit behavior Mode StageMode // Maximum time allowed for clone operation in seconds (0 means no limit) From d578da7bc993e2dd2bbdf8b6180fce387c484941 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Thu, 24 Jul 2025 19:37:02 +0200 Subject: [PATCH 074/101] auth: Restore Authentication UI access (#108625) Restore Authentication UI access --- public/app/routes/routes.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 09cdc5d94a5..334087cfbed 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -301,13 +301,9 @@ export function getAppRoutes(): RouteDescriptor[] { { path: '/admin/authentication', roles: () => contextSrv.evaluatePermission([AccessControlAction.SettingsWrite]), - component: - config.licenseInfo.enabledFeatures?.saml || config.ldapEnabled - ? SafeDynamicImport( - () => - import(/* webpackChunkName: "AdminAuthentication" */ '../features/auth-config/AuthProvidersListPage') - ) - : () => , + component: SafeDynamicImport( + () => import(/* webpackChunkName: "AdminAuthentication" */ '../features/auth-config/AuthProvidersListPage') + ), }, { path: '/admin/authentication/ldap', From a5a45d44768d45a5e1e7595720f72d75ff90d579 Mon Sep 17 00:00:00 2001 From: Jacob Valdez Date: Thu, 24 Jul 2025 13:05:22 -0500 Subject: [PATCH 075/101] Docs: removing static options whats new post (#108628) --- docs/sources/whatsnew/whats-new-in-v12-1.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/sources/whatsnew/whats-new-in-v12-1.md b/docs/sources/whatsnew/whats-new-in-v12-1.md index 93bd6fb093e..27e4f848682 100644 --- a/docs/sources/whatsnew/whats-new-in-v12-1.md +++ b/docs/sources/whatsnew/whats-new-in-v12-1.md @@ -27,7 +27,6 @@ posts: items: - whats-new/2025-06-30-transformations-regression-analysis.md - whats-new/2025-07-14-custom-variable-support-in-visualization-actions.md - - whats-new/2025-07-14-static-options-for-query-variable.md - whats-new/2025-07-10-server-configurable-quick-time-ranges-for-dashboards.md - whats-new/2025-07-14-enhanced-custom-currency-format-display-exact-financial-values.md - title: Authentication and authorization From 0723a51e87dea4d41af7359f5bd430f2a5524afc Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Thu, 24 Jul 2025 20:12:43 +0200 Subject: [PATCH 076/101] Prometheus: Fix adding labels to the query with empty values (#108611) * values can be empty * remove a test --- .../grafana-prometheus/src/add_label_to_query.ts | 2 +- .../grafana-prometheus/src/language_utils.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/grafana-prometheus/src/add_label_to_query.ts b/packages/grafana-prometheus/src/add_label_to_query.ts index 7c75e6e14ec..06e82fcbf00 100644 --- a/packages/grafana-prometheus/src/add_label_to_query.ts +++ b/packages/grafana-prometheus/src/add_label_to_query.ts @@ -21,7 +21,7 @@ import { PromVisualQuery } from './querybuilder/types'; * @param operator */ export function addLabelToQuery(query: string, key: string, value: string | number, operator = '='): string { - if (!key || !value) { + if (!key) { throw new Error('Need label to add to query.'); } diff --git a/packages/grafana-prometheus/src/language_utils.test.ts b/packages/grafana-prometheus/src/language_utils.test.ts index 5ba14363da7..555634f898e 100644 --- a/packages/grafana-prometheus/src/language_utils.test.ts +++ b/packages/grafana-prometheus/src/language_utils.test.ts @@ -191,6 +191,18 @@ describe('expandRecordingRules()', () => { const result = expandRecordingRules(query, mapping); expect(result).toBe(expected); }); + + it('when there is an empty label value it should still be able to expand the rule', () => { + const query = `sum(max by (cluster, container) (pod_cpu:active:kube_limits{container!="", cluster=~"pink"}))`; + const mapping = { + 'pod_cpu:active:kube_limits': { + expandedQuery: `kube_limits{job!="", resource="cpu"} * on (namespace, pod, cluster) group_left () max by (namespace, pod, cluster) ((kube_pod_status_phase{phase=~"Pending|Running"} == 1))`, + }, + }; + const expected = `sum(max by (cluster, container) (kube_limits{job!="", resource="cpu", container!="", cluster=~"pink"} * on (namespace, pod, cluster) group_left () max by (namespace, pod, cluster) ((kube_pod_status_phase{phase=~"Pending|Running", container!="", cluster=~"pink"} == 1))))`; + const result = expandRecordingRules(query, mapping); + expect(result).toBe(expected); + }); }); describe('escapeLabelValueInExactSelector()', () => { From ab92e2cbb615e7ab13a2dd65eb6d586c387689f6 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:51:21 -0500 Subject: [PATCH 077/101] Transformations: Take nanos into account for Sort by (#108564) * chore(sortDataFrame): sort by nanos --- .../src/dataframe/processDataFrame.test.ts | 53 +++++++++++++++++++ .../grafana-data/src/field/fieldComparers.ts | 25 +++++---- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/packages/grafana-data/src/dataframe/processDataFrame.test.ts b/packages/grafana-data/src/dataframe/processDataFrame.test.ts index 7fab4d0bfb2..72b3041d87c 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.test.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.test.ts @@ -388,6 +388,59 @@ describe('sorted DataFrame', () => { }); }); +describe('sorted DataFrame by nanos', () => { + it('Should sort nanos with numeric timestamp', () => { + const frame = toDataFrame({ + fields: [ + { name: 'first', type: FieldType.time, values: [1, 1, 2, 2, 3, 3], nanos: [100, 102, 1, 2, 1000, 999] }, + { name: 'second', type: FieldType.string, values: ['a', 'b', 'c', 'd', 'e', 'f'] }, + ], + }); + + const sorted = sortDataFrame(frame, 0, true); + expect(sorted.length).toEqual(6); + expect(sorted.fields[0].values).toEqual([3, 3, 2, 2, 1, 1]); + expect(sorted.fields[1].values).toEqual(['e', 'f', 'd', 'c', 'b', 'a']); + }); + + it('Should sort by nanos with dateTime timestamp', () => { + const frame = toDataFrame({ + fields: [ + { + name: 'first', + type: FieldType.time, + values: [dateTime(50), dateTime(50), dateTime(100)], + nanos: [1, 0, 100], + }, + { name: 'second', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const sorted = sortDataFrame(frame, 0); + expect(sorted.length).toEqual(3); + expect(sorted.fields[0].values).toEqual([dateTime(50), dateTime(50), dateTime(100)]); + expect(sorted.fields[1].values).toEqual(['b', 'a', 'c']); + }); + + // Not sure if we expect nanos to exist on any field type besides time fields, but the schema allows it. + // Keep in mind if sorting by a non time field, the nanos is expected to present on the field that is being sorted + it('Should sort by nanos with string timestamp', () => { + const frame = toDataFrame({ + fields: [ + { name: 'fist', type: FieldType.time, values: [1, 2, 3, 3] }, + { name: 'second', type: FieldType.string, values: ['a', 'b', 'b', 'c'], nanos: [100, 0, 1, 0] }, + { name: 'third', type: FieldType.number, values: [2000, 3000, 1000] }, + ], + }); + + const sorted = sortDataFrame(frame, 1, true); + expect(sorted.length).toEqual(4); + expect(sorted.fields[0].values).toEqual([3, 2, 3, 1]); + expect(sorted.fields[0].nanos).toBeUndefined(); + expect(sorted.fields[1].values).toEqual(['c', 'b', 'b', 'a']); + }); +}); + describe('reverse DataFrame', () => { const frame = toDataFrame({ fields: [ diff --git a/packages/grafana-data/src/field/fieldComparers.ts b/packages/grafana-data/src/field/fieldComparers.ts index 94bb4312b14..7b3e64bbc6e 100644 --- a/packages/grafana-data/src/field/fieldComparers.ts +++ b/packages/grafana-data/src/field/fieldComparers.ts @@ -17,9 +17,9 @@ export const fieldIndexComparer = (field: Field, reverse = false): IndexComparer return booleanIndexComparer(values, reverse); case FieldType.time: if (typeof field.values[0] === 'number') { - return timestampIndexComparer(values, reverse); + return timestampIndexComparer(values, reverse, field.nanos); } - return timeIndexComparer(values, reverse); + return timeIndexComparer(values, reverse, field.nanos); default: return naturalIndexComparer(reverse); } @@ -79,17 +79,24 @@ const falsyComparer = (a: unknown, b: unknown): number => { return 0; }; -const timestampIndexComparer = (values: number[], reverse: boolean): IndexComparer => { +const timestampIndexComparer = (values: number[], reverse: boolean, nanos?: number[]): IndexComparer => { let mult = reverse ? -1 : 1; + + if (nanos !== undefined) { + return (a: number, b: number): number => mult * (values[a] - values[b] || nanos[a] - nanos[b]); + } + return (a: number, b: number): number => mult * (values[a] - values[b]); }; -const timeIndexComparer = (values: unknown[], reverse: boolean): IndexComparer => { - return (a: number, b: number): number => { - const vA = values[a]; - const vB = values[b]; - return reverse ? timeComparer(vB, vA) : timeComparer(vA, vB); - }; +const timeIndexComparer = (values: unknown[], reverse: boolean, nanos?: number[]): IndexComparer => { + const mult = reverse ? -1 : 1; + + if (nanos !== undefined) { + return (a: number, b: number): number => mult * (timeComparer(values[a], values[b]) || nanos[a] - nanos[b]); + } + + return (a: number, b: number): number => mult * timeComparer(values[a], values[b]); }; const booleanIndexComparer = (values: boolean[], reverse: boolean): IndexComparer => { From 16aee8047c9ad2c7881997d6747444cc36bcf2a7 Mon Sep 17 00:00:00 2001 From: "Arati R." <33031346+suntala@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:38:35 +0200 Subject: [PATCH 078/101] Unified Search: Randomise instance selected by distributor (#108619) * Randomise instance selected by distributor * add namespace to distributor logging --------- Co-authored-by: Will Assis --- .../unified/resource/search_server_distributor.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/storage/unified/resource/search_server_distributor.go b/pkg/storage/unified/resource/search_server_distributor.go index f410207b59e..3fc0203282d 100644 --- a/pkg/storage/unified/resource/search_server_distributor.go +++ b/pkg/storage/unified/resource/search_server_distributor.go @@ -3,6 +3,7 @@ package resource import ( "context" "hash/fnv" + "math/rand" "time" "github.com/grafana/dskit/ring" @@ -138,7 +139,9 @@ func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, n return ctx, nil, err } - client, err := ds.clientPool.GetClientForInstance(rs.Instances[0]) + // Randomly select an instance for primitive load balancing + inst := rs.Instances[rand.Intn(len(rs.Instances))] + client, err := ds.clientPool.GetClientForInstance(inst) if err != nil { return ctx, nil, err } @@ -148,9 +151,9 @@ func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, n md = make(metadata.MD) } - ds.log.Info("distributing request to ", "methodName", methodName, "instanceId", rs.Instances[0].Id) + ds.log.Info("distributing request to ", "methodName", methodName, "instanceId", inst.Id, "namespace", namespace) - _ = grpc.SetHeader(ctx, metadata.Pairs("proxied-instance-id", rs.Instances[0].Id)) + _ = grpc.SetHeader(ctx, metadata.Pairs("proxied-instance-id", inst.Id)) return userutils.InjectOrgID(metadata.NewOutgoingContext(ctx, md), namespace), client.(*RingClient).Client, nil } From 227a33fadd6700933619cdc8f51a8ad16c025cc0 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:39:01 -0400 Subject: [PATCH 079/101] Docs: Document cell options by cell type (#106961) --- .../visualizations/table/index.md | 151 ++++++++++-------- .../shared/visualizations/cell-options.md | 10 ++ 2 files changed, 95 insertions(+), 66 deletions(-) create mode 100644 docs/sources/shared/visualizations/cell-options.md diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index eb47c849ca1..9a7f97b13e4 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -59,6 +59,11 @@ refs: destination: /docs/grafana//panels-visualizations/query-transform-data/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/ + graph-styles: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/visualizations/time-series/#graph-styles-options + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/time-series/#graph-styles-options --- # Table @@ -217,11 +222,8 @@ After you activate the table footer, make selections for the following options: ### Cell options Cell options allow you to control how data is displayed in a table. -The options are: - -- [Cell type](#cell-type) - Control the default cell display settings. -- [Wrap text](#wrap-text) - Wrap text in the cell that contains the longest content in your table. -- [Cell value inspect](#cell-value-inspect) - Enables value inspection from table cells. +The options are differ based on the cell type that you select and are outlined within the descriptions of each cell type. +The following table provides short descriptions for each cell type and links to a longer description and the cell type options. #### Cell type @@ -231,50 +233,88 @@ Additional configuration is available for some cell types. If you want to apply a cell type to only some fields instead of all fields, you can do so using the **Cell options > Cell type** field override. -| Cell type | Description | -| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Auto | Automatically displays values with sensible defaults applied. | -| [Sparkline](#sparkline) | Shows values rendered as a sparkline. | -| [Colored text](#colored-text) | If thresholds are set, then the field text is displayed in the appropriate threshold color. | -| [Colored background](#colored-background) | If thresholds are set, then the field background is displayed in the appropriate threshold color. | -| [Gauge](#gauge) | Cells can be displayed as a graphical gauge, with several different presentation types. You can set the [Gauge display mode](#gauge-display-mode) and the [Value display](#value-display) options. | + +| Cell type | Description | +| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| [Auto](#auto) | A basic text and number cell. | +| [Sparkline](#sparkline) | Shows values rendered as a sparkline. | +| [Colored text](#colored-text) | If thresholds, value mappings, or color schemes are set, then the cell text is displayed in the appropriate color. | +| [Colored background](#colored-background) | If thresholds, value mappings, or color schemes are set, then the cell background is displayed in the appropriate color. | +| [Gauge](#gauge) | Values are displayed as a horizontal bar gauge. You can set the [Gauge display mode](#gauge-display-mode) and the [Value display](#value-display) options. | | Data links | If you've configured data links, when the cell type is **Auto**, the cell text becomes clickable. If you change the cell type to **Data links**, the cell text reflects the titles of the configured data links. To control the application of data link text more granularly, use a **Cell option > Cell type > Data links** field override. | -| [JSON View](#json-view) | Shows values formatted as code. | -| [Image](#image) | If the field value is an image URL or a base64 encoded image, the table displays the image. | -| [Actions](#actions) | The cell displays a button that triggers a basic, unauthenticated API call when clicked. | +| [JSON View](#json-view) | Shows values formatted as code. | +| [Image](#image) | Displays an image when the value is a URL or a base64 encoded image. | +| [Actions](#actions) | The cell displays a button that triggers a basic, unauthenticated API call when clicked. | + -##### Sparkline +#### Auto + +This is a basic text and number cell. + +It has the following cell options: + +{{< docs/shared lookup="visualizations/cell-options.md" source="grafana" version="" >}} + +#### Sparkline This cell type shows values rendered as a sparkline. To show sparklines on data with multiple time series, use the [Time series to table transformation](ref:time-series-to-table-transformation) to process it into a format the table can show. ![Table using sparkline cell type](/media/docs/grafana/panels-visualizations/screenshot-table-as-sparkline-v11.3.png) -You can customize sparklines with many of the same options as the [time series visualization](ref:time-series-panel) including line style and width, fill opacity, gradient mode, and more. -You can also change the color of the sparkline by updating the [color scheme](ref:color-scheme) in the **Standard options** section of the panel configuration. +The sparkline cell type options are described in the following table. +For more detailed information about all of the sparkline styling options (except **Hide value**), refer to the [time series graph styles documentation](ref:graph-styles). -##### Colored text + +| Option | Description | +| ------------------- | --------------------------------------------------------------------------------------------- | +| Hide value | Toggle the switch on or off to display or hide the cell value on the sparkline. | +| Style | Choose whether to display your time-series data as **Lines**, **Bars**, or **Points**. You can use overrides to combine multiple styles in the same graph. | +| Line interpolation | How the graph interpolates the series line. Choose from:
  • **Linear** - Points are joined by straight lines.
  • **Smooth** - Points are joined by curved lines that smooths transitions between points.
  • **Step before** - The line is displayed as steps between points. Points are rendered at the end of the step.
  • **Step after** - The line is displayed as steps between points. Points are rendered at the beginning of the step.
| +| Line width | The thickness of the series lines or the outline for bars using the **Line width** slider. | +| Fill opacity | The series area fill color using the **Fill opacity** slider. | +| Gradient mode | Gradient mode controls the gradient fill, which is based on the series color. Gradient appearance is influenced by the **Fill opacity** setting. To change the color, use the standard color scheme field option. For more information, refer to [Color scheme](ref:color-scheme). Choose from:
  • **None** - No gradient fill. This is the default setting.
  • **Opacity** - An opacity gradient where the opacity of the fill increases as y-axis values increase.
  • **Hue** - A subtle gradient that's based on the hue of the series color.
| +| Line style | Choose from:
  • **Solid**
  • **Dash** - Select the length and gap for the line dashes. Default dash spacing is 10, 10.
  • **Dots** - Select the gap for the dot spacing. Default dot spacing is 0, 10.
| +| Connect null values | How null values, which are gaps in the data, appear on the graph. Null values can be connected to form a continuous line or set to a threshold above which gaps in the data are no longer connected. Choose from:
  • **Never** - Time series data points with gaps in the data are never connected.
  • **Always** - Time series data points with gaps in the data are always connected.
  • **Threshold** - Specify a threshold above which gaps in the data are no longer connected. This can be useful when the connected gaps in the data are of a known size or within a known range, and gaps outside this range should no longer be connected.
| +| Show points | Whether to show data points to lines or bars. Choose from:
  • **Auto** - Grafana determines a point's visibility based on the density of the data. If the density is low, then points appear.
  • **Always** - Show the points regardless of how dense the dataset is.
  • **Never** - Don't show points.
| +| Point size | Set the size of the points, from 1 to 40 pixels in diameter. | +| Bar alignment | Set the position of the bar relative to a data point. | + -If thresholds are set, with this cell type, the field text is displayed in the appropriate threshold color. +#### Colored text + +If thresholds, value mappings, or color schemes are set, the cell text is displayed in the appropriate color. ![Table with colored text cell type](/media/docs/grafana/panels-visualizations/screenshot-table-colored-text-v11.3-2.png) -{{< admonition type="note" >}} -This is an experimental feature. -{{< /admonition >}} +The colored text cell type has the following options: -##### Colored background +{{< docs/shared lookup="visualizations/cell-options.md" source="grafana" version="" >}} -If thresholds are set, with this cell type, the field background is displayed in the appropriate threshold color. +#### Colored background + +If thresholds, value mappings, or color schemes are set, the cell background is displayed in the appropriate color. ![Table with colored background cell type](/media/docs/grafana/panels-visualizations/screenshot-table-colored-bkgrnd-v11.3-2.png) -- **Background display mode** - Choose between **Basic** and **Gradient**. -- **Apply to entire row** - Toggle the switch on to apply the background color that's configured for the cell to the whole row. +You can also set background cell color by row: ![Table with background cell color applied to row](/media/docs/grafana/panels-visualizations/screenshot-table-colored-row-v11.3.png) -##### Gauge +The colored background cell type has the following options: + + +| Option | Description | +| ------ | ----------- | +| Background display mode | Choose between **Basic** and **Gradient**. | +| Apply to entire row | Toggle the switch on to apply the background color that's configured for the cell to the whole row. | +| Wrap text |

Toggle the **Wrap text** switch to wrap text in the cell that contains the longest content in your table. To wrap the text _in a specific column only_, use a **Fields with name** [field override](ref:field-override), select the **Cell options > Cell type** override property, and toggle on the **Wrap text** switch.

Text wrapping is in [public preview](https://grafana.com/docs/release-life-cycle/#public-preview), however, it’s available to use by default.

| +| Cell value inspect |

Enables value inspection from table cells. When the switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer which contains two tabs: **Plain text** and **Code editor**.

Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. However, you can switch back and forth between tabs.

| + + + + +#### Gauge With this cell type, cells can be displayed as a graphical gauge, with several different presentation types controlled by the [gauge display mode](#gauge-display-mode) and the [value display](#value-display). @@ -283,53 +323,58 @@ The maximum and minimum values of the gauges are configured automatically from t If you don't want the max/min values to be pulled from the whole dataset, you can configure them for each column using [field overrides](#field-overrides). {{< /admonition >}} -###### Gauge display mode +##### Gauge display mode You can set three gauge display modes. - | Option | Description | | ------ | ----------- | | Basic | Shows a simple gauge with the threshold levels defining the color of gauge. {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-gauge-mode-basic-v11.3.png" alt="Table cell with basic gauge mode" >}} | | Gradient | The threshold levels define a gradient. {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-gauge-mode-gradient-v11.3.png" alt="Table cell with gradient gauge mode" >}} | | Retro LCD | The gauge is split up in small cells that are lit or unlit. {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-gauge-mode-retro-v11.3.png" alt="Table cell with retro LCD gauge mode" >}} | - -###### Value display +##### Value display Labels displayed alongside of the gauges can be set to be colored by value, match the theme text color, or be hidden. - | Option | Description | | ------ | ----------- | | Value color | Labels are colored by value. {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-labels-value-color-v11.3.png" alt="Table with labels in value color" >}} | | Text color | Labels match the theme text color. {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-labels-text-color-v11.3.png" alt="Table with labels in theme color" >}} | | Hidden | Labels are hidden. {{< figure src="/media/docs/grafana/panels-visualizations/screenshot-labels-hidden-v11.3.png" alt="Table with labels hidden" >}} | - -##### JSON View +#### JSON View This cell type shows values formatted as code. If a value is an object the JSON view allowing browsing the JSON object will appear on hover. {{< figure src="/static/img/docs/tables/json-view.png" max-width="350px" alt="JSON view" class="docs-image--no-shadow" >}} -##### Image +For the JSON view cell type, you can set enable **Cell value inspect**. +This enables value inspection from table cells. +When the switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer which contains two tabs: **Plain text** and **Code editor**. + +Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. +However, you can switch back and forth between tabs + +#### Image If you have a field value that is an image URL or a base64 encoded image, this cell type displays it as an image. ![Table with image cell type](/media/docs/grafana/panels-visualizations/screenshot-table-cell-image-v11.3.png) -Set the following options: +It has the following options: -- **Alt text** - Set the alternative text of an image. The text will be available for screen readers and in cases when images can't be loaded. -- **Title text** - Set the text that's displayed when the image is hovered over with a cursor. +| Option | Description | +| ---------- | ----------------------------------------------------------------------------------------------------------------------------- | +| Alt text | Set the alternative text of an image. The text will be available for screen readers and in cases when images can't be loaded. | +| Title text | Set the text that's displayed when the image is hovered over with a cursor. | -##### Actions +#### Actions The cell displays a button that triggers a basic, unauthenticated API call when clicked. Configure the API call with the following options: @@ -343,34 +388,8 @@ Configure the API call with the following options: | Query parameters | Enter as many **Key**, **Value** pairs as you need. | | Header parameters | Enter as many **Key**, **Value** pairs as you need. | | Payload | Enter the body of the API call. | - -#### Wrap text - -{{< admonition type="note" >}} -Text wrapping is in [public preview](https://grafana.com/docs/release-life-cycle/#public-preview), however, it’s available to use by default. -We’d love hear from you about how this new feature is working. To provide feedback, you can open an issue in the [Grafana GitHub repository](https://github.com/grafana/grafana). -{{< /admonition >}} - -Toggle the **Wrap text** switch to wrap text in the cell that contains the longest content in your table. - -To wrap the text _in a specific column only_, use a **Fields with name** [field override](ref:field-override), select the **Cell options > Cell type** override property, and toggle on the **Wrap text** switch. - -This option is available for the following cell types: **Auto**, **Colored text**, and **Colored background**. - -#### Cell value inspect - -Enables value inspection from table cells. When the **Cell inspect value** switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer. - -The **Inspect value** drawer has two tabs, **Plain text** and **Code editor**. -Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. -However, you can switch back and forth between tabs. - -This option is available for the following cell types: **Auto**, **Colored text**, **Colored background**, and **JSON View**. - -If you want to apply this setting to only some fields instead of all fields, you can do so using the **Cell options > Cell value inspect** field override. - ### Standard options {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} diff --git a/docs/sources/shared/visualizations/cell-options.md b/docs/sources/shared/visualizations/cell-options.md new file mode 100644 index 00000000000..03c0e4a76dc --- /dev/null +++ b/docs/sources/shared/visualizations/cell-options.md @@ -0,0 +1,10 @@ +--- +title: Cell options +--- + + +| Option | Description | +| ------ | ----------- | +| Wrap text |

Toggle the **Wrap text** switch to wrap text in the cell that contains the longest content in your table. To wrap the text _in a specific column only_, use a **Fields with name** [field override](https://grafana.com/docs/grafana//panels-visualizations/configure-overrides/), select the **Cell options > Cell type** override property, and toggle on the **Wrap text** switch.

Text wrapping is in [public preview](https://grafana.com/docs/release-life-cycle/#public-preview), however, it’s available to use by default.

| +| Cell value inspect |

Enables value inspection from table cells. When the switch is toggled on, clicking the inspect icon in a cell opens the **Inspect value** drawer which contains two tabs: **Plain text** and **Code editor**.

Grafana attempts to automatically detect the type of data in the cell and opens the drawer with the associated tab showing. However, you can switch back and forth between tabs.

| + From 183cd0843eb6158f983f9e852d3c79a526288dbc Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:39:12 -0400 Subject: [PATCH 080/101] keep instance in the ring on shutdown (#108609) --- pkg/storage/unified/sql/service.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/storage/unified/sql/service.go b/pkg/storage/unified/sql/service.go index 171895cb67b..cdc6ee97ca3 100644 --- a/pkg/storage/unified/sql/service.go +++ b/pkg/storage/unified/sql/service.go @@ -155,6 +155,8 @@ func ProvideUnifiedStorageGrpcService( if err != nil { return nil, fmt.Errorf("failed to initialize storage-ring lifecycler: %s", err) } + + s.ringLifecycler.SetKeepInstanceInTheRingOnShutdown(true) subservices = append(subservices, s.ringLifecycler) } From d3b6d7c45e1e3b623b8a48ae1e851958a355ac97 Mon Sep 17 00:00:00 2001 From: Tania <10127682+undef1nd@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:45:22 +0200 Subject: [PATCH 081/101] OpenFeature: Add config setting to disable API (#108620) * Add config option to disable ofrep api * Apply review feedback --- conf/defaults.ini | 1 + pkg/registry/apis/ofrep/register.go | 5 ++++- pkg/setting/setting_openfeature.go | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index ccd2ba2d73a..9baa84e5760 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -2016,6 +2016,7 @@ enable = [feature_toggles.openfeature] # This is EXPERIMENTAL. Please, do not use this section +enable_api = true provider = static [feature_toggles.openfeature.context] diff --git a/pkg/registry/apis/ofrep/register.go b/pkg/registry/apis/ofrep/register.go index 684e1f402e1..154e3f6b6fc 100644 --- a/pkg/registry/apis/ofrep/register.go +++ b/pkg/registry/apis/ofrep/register.go @@ -61,9 +61,12 @@ 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 { staticEvaluator, err = featuremgmt.CreateStaticEvaluator(cfg) if err != nil { diff --git a/pkg/setting/setting_openfeature.go b/pkg/setting/setting_openfeature.go index 13131f89111..a817dc28a80 100644 --- a/pkg/setting/setting_openfeature.go +++ b/pkg/setting/setting_openfeature.go @@ -11,6 +11,7 @@ const ( ) type OpenFeatureSettings struct { + APIEnabled bool ProviderType string URL *url.URL TargetingKey string @@ -21,6 +22,7 @@ func (cfg *Cfg) readOpenFeatureSettings() error { cfg.OpenFeature = OpenFeatureSettings{} config := cfg.Raw.Section("feature_toggles.openfeature") + cfg.OpenFeature.APIEnabled = config.Key("enable_api").MustBool(true) cfg.OpenFeature.ProviderType = config.Key("provider").MustString(StaticProviderType) cfg.OpenFeature.TargetingKey = config.Key("targetingKey").MustString(cfg.AppURL) From 219672722662938633ffae3f6e8bfa89f8f29661 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:07:28 -0700 Subject: [PATCH 082/101] TimeSeries: Add Time Comparison (#104672) * TimeComparison: Add time comparison option * Display control conditionally based on option * Provide timeCompare option to panels that opt in * Only render time compare if panel supports it * Create a custom time range compare component * Move custom component * Remove button checked * Move time compare options out of field config * Fix types and remove from fieldConfigProperty enum * Remove from field config registry * Add headerActions to V2 schema * Remove dependency on timeCompare support registry * Add feature flag * Subscribe to options change in wrapper * Remove custom rendering to simplify wrapper * Bump scenes version * Hide checkbox * Remove unneeded query refresh during disable * Fix import order in timeseries module * Add util tests * Support time comparison toggle for new viz panels * Revert grafana-app-sdk and manifest changes * Revert yarn lock --- .../src/types/featureToggles.gen.ts | 4 + .../grafana-schema/src/common/common.gen.ts | 10 + .../grafana-schema/src/common/timecompare.cue | 7 + .../x/TimeSeriesPanelCfg_types.gen.ts | 1 + .../grafana-ui/src/options/builder/index.ts | 1 + .../src/options/builder/timeCompare.tsx | 19 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 855 ++++++------------ .../scene/CustomTimeRangeCompare.tsx | 60 ++ .../layoutSerializers/utils.test.ts | 78 +- .../serialization/layoutSerializers/utils.ts | 4 + .../transformSaveModelToScene.test.ts | 35 + .../transformSaveModelToScene.ts | 4 + .../features/dashboard-scene/utils/utils.ts | 4 + .../app/plugins/panel/timeseries/module.tsx | 5 + .../app/plugins/panel/timeseries/panelcfg.cue | 1 + .../plugins/panel/timeseries/panelcfg.gen.ts | 1 + 19 files changed, 531 insertions(+), 570 deletions(-) create mode 100644 packages/grafana-schema/src/common/timecompare.cue create mode 100644 packages/grafana-ui/src/options/builder/timeCompare.tsx create mode 100644 public/app/features/dashboard-scene/scene/CustomTimeRangeCompare.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 3316e91792d..15b138e4ff7 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -401,6 +401,10 @@ export interface FeatureToggles { */ canvasPanelPanZoom?: boolean; /** + * Enables time comparison option in supported panels + */ + timeComparison?: boolean; + /** * Enables infinite scrolling for the Logs panel in Explore and Dashboards * @default true */ diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index 7efc5e3ff60..b98c83c9525 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -882,6 +882,16 @@ export type TimeZoneUtc = 'utc'; */ export type TimeZoneBrowser = 'browser'; +/** + * Options for time comparison + */ +export interface TimeCompareOptions { + /** + * Enable time comparison control + */ + timeCompare?: boolean; +} + /** * Optional formats for the template variable replace functions * See also https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options diff --git a/packages/grafana-schema/src/common/timecompare.cue b/packages/grafana-schema/src/common/timecompare.cue new file mode 100644 index 00000000000..b85287c9301 --- /dev/null +++ b/packages/grafana-schema/src/common/timecompare.cue @@ -0,0 +1,7 @@ +package common + +// Options for time comparison +TimeCompareOptions: { + // Enable time comparison control + timeCompare?: bool +} @cuetsy(kind="interface") \ No newline at end of file diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index b34b0b7e301..8b9d3d48053 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -15,6 +15,7 @@ export const pluginVersion = "12.1.0-pre"; export interface Options extends common.OptionsWithTimezones { legend: common.VizLegendOptions; orientation?: common.VizOrientation; + timeCompare?: common.TimeCompareOptions; tooltip: common.VizTooltipOptions; } diff --git a/packages/grafana-ui/src/options/builder/index.ts b/packages/grafana-ui/src/options/builder/index.ts index 42dd18e4850..98b376794ee 100644 --- a/packages/grafana-ui/src/options/builder/index.ts +++ b/packages/grafana-ui/src/options/builder/index.ts @@ -4,3 +4,4 @@ export * from './legend'; export { addTooltipOptions } from './tooltip'; export * from './text'; export * from './stacking'; +export { addTimeCompareOption } from './timeCompare'; diff --git a/packages/grafana-ui/src/options/builder/timeCompare.tsx b/packages/grafana-ui/src/options/builder/timeCompare.tsx new file mode 100644 index 00000000000..de19457d80f --- /dev/null +++ b/packages/grafana-ui/src/options/builder/timeCompare.tsx @@ -0,0 +1,19 @@ +import { PanelOptionsEditorBuilder } from '@grafana/data'; +import { TimeCompareOptions } from '@grafana/schema'; + +/** + * Adds a generic time comparison option to the panel options editor. + * Can be used by any panel that supports time comparison. + */ +export function addTimeCompareOption( + builder: PanelOptionsEditorBuilder, + defaultValue = false +) { + builder.addBooleanSwitch({ + path: 'timeCompare', + name: 'Enable', + category: ['Time Comparison'], + description: '', + defaultValue, + }); +} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 8958d488a3c..4fdb2c87c27 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -667,6 +667,13 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, }, + { + Name: "timeComparison", + Description: "Enables time comparison option in supported panels", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDatavizSquad, + }, { Name: "logsInfiniteScrolling", Description: "Enables infinite scrolling for the Logs panel in Explore and Dashboards", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b4cc812bf54..531ac398257 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -88,6 +88,7 @@ dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true +timeComparison,experimental,@grafana/dataviz-squad,false,false,true logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 43a51188253..96930b299c9 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -363,6 +363,10 @@ const ( // Allow pan and zoom in canvas panel FlagCanvasPanelPanZoom = "canvasPanelPanZoom" + // FlagTimeComparison + // Enables time comparison option in supported panels + FlagTimeComparison = "timeComparison" + // FlagLogsInfiniteScrolling // Enables infinite scrolling for the Logs panel in Explore and Dashboards FlagLogsInfiniteScrolling = "logsInfiniteScrolling" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index c1915403da5..08c49aac219 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -6,7 +6,7 @@ { "metadata": { "name": "addFieldFromCalculationStatFunctions", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-03T14:39:58Z" }, "spec": { @@ -20,7 +20,7 @@ { "metadata": { "name": "aiGeneratedDashboardChanges", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-03-05T12:01:31Z" }, "spec": { @@ -33,7 +33,7 @@ { "metadata": { "name": "alertEnrichment", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-06-06T12:16:07Z" }, "spec": { @@ -48,7 +48,7 @@ { "metadata": { "name": "alertRuleRestore", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-05T14:15:26Z" }, "spec": { @@ -61,7 +61,7 @@ { "metadata": { "name": "alertRuleUseFiredAtForStartsAt", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-22T11:16:38Z" }, "spec": { @@ -74,11 +74,8 @@ { "metadata": { "name": "alertingAIAnalyzeCentralStateHistory", - "resourceVersion": "1752215431801", - "creationTimestamp": "2025-07-07T14:40:14Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-11 06:30:31.801024 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enable AI-analyze central state history.", @@ -92,8 +89,8 @@ { "metadata": { "name": "alertingAIFeedback", - "resourceVersion": "1752820131286", - "creationTimestamp": "2025-07-18T06:28:51Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enable AI-generated feedback from the Grafana UI.", @@ -107,11 +104,8 @@ { "metadata": { "name": "alertingAIGenAlertRules", - "resourceVersion": "1752215431801", - "creationTimestamp": "2025-07-07T14:40:14Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-11 06:30:31.801024 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enable AI-generated alert rules.", @@ -125,11 +119,8 @@ { "metadata": { "name": "alertingAIGenTemplates", - "resourceVersion": "1752215431801", - "creationTimestamp": "2025-07-07T14:40:14Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-11 06:30:31.801024 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enable AI-generated alerting templates.", @@ -143,11 +134,8 @@ { "metadata": { "name": "alertingAIImproveAlertRules", - "resourceVersion": "1752215431801", - "creationTimestamp": "2025-07-07T14:40:14Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-11 06:30:31.801024 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enable AI-improve alert rules labels and annotations.", @@ -161,7 +149,7 @@ { "metadata": { "name": "alertingBacktesting", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-12-14T14:44:14Z" }, "spec": { @@ -173,7 +161,7 @@ { "metadata": { "name": "alertingBulkActionsInUI", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-24T14:49:59Z" }, "spec": { @@ -189,7 +177,7 @@ { "metadata": { "name": "alertingCentralAlertHistory", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-05-29T15:01:38Z" }, "spec": { @@ -202,7 +190,7 @@ { "metadata": { "name": "alertingDisableSendAlertsExternal", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-05-23T12:29:19Z" }, "spec": { @@ -216,7 +204,7 @@ { "metadata": { "name": "alertingFilterV2", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-09-11T11:29:26Z" }, "spec": { @@ -229,7 +217,7 @@ { "metadata": { "name": "alertingImportAlertmanagerAPI", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-06-10T08:32:50Z" }, "spec": { @@ -244,7 +232,7 @@ { "metadata": { "name": "alertingImportYAMLUI", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-05-21T15:59:41Z" }, "spec": { @@ -258,7 +246,7 @@ { "metadata": { "name": "alertingJiraIntegration", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-02-14T12:22:04Z" }, "spec": { @@ -272,7 +260,7 @@ { "metadata": { "name": "alertingListViewV2", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-05-24T14:40:49Z" }, "spec": { @@ -285,7 +273,7 @@ { "metadata": { "name": "alertingListViewV2PreviewToggle", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-22T08:50:34Z" }, "spec": { @@ -298,7 +286,7 @@ { "metadata": { "name": "alertingMigrationUI", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-14T16:40:05Z" }, "spec": { @@ -312,8 +300,8 @@ { "metadata": { "name": "alertingNotificationHistory", - "resourceVersion": "1752682072771", - "creationTimestamp": "2025-07-16T16:07:52Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enables the notification history feature", @@ -327,7 +315,7 @@ { "metadata": { "name": "alertingNotificationsStepMode", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-22T11:07:45Z" }, "spec": { @@ -341,7 +329,7 @@ { "metadata": { "name": "alertingPrometheusRulesPrimary", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-09-27T12:27:16Z" }, "spec": { @@ -368,7 +356,7 @@ { "metadata": { "name": "alertingQueryAndExpressionsStepMode", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-09-26T06:33:14Z" }, "spec": { @@ -382,7 +370,7 @@ { "metadata": { "name": "alertingQueryOptimization", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-01-10T20:52:58Z" }, "spec": { @@ -395,7 +383,7 @@ { "metadata": { "name": "alertingRulePermanentlyDelete", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-03T11:18:25Z" }, "spec": { @@ -411,7 +399,7 @@ { "metadata": { "name": "alertingRuleRecoverDeleted", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-27T14:39:26Z" }, "spec": { @@ -427,7 +415,7 @@ { "metadata": { "name": "alertingRuleVersionHistoryRestore", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-02-17T12:25:32Z" }, "spec": { @@ -443,7 +431,7 @@ { "metadata": { "name": "alertingSaveStateCompressed", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-01-27T17:47:33Z" }, "spec": { @@ -456,7 +444,7 @@ { "metadata": { "name": "alertingSaveStatePeriodic", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-01-23T16:03:30Z" }, "spec": { @@ -468,7 +456,7 @@ { "metadata": { "name": "alertingUIOptimizeReducer", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-18T10:59:00Z" }, "spec": { @@ -482,7 +470,7 @@ { "metadata": { "name": "alertmanagerRemotePrimary", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -494,7 +482,7 @@ { "metadata": { "name": "alertmanagerRemoteSecondary", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-30T16:27:08Z" }, "spec": { @@ -503,25 +491,10 @@ "codeowner": "@grafana/alerting-squad" } }, - { - "metadata": { - "name": "angularDeprecationUI", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-08-29T14:05:47Z", - "deletionTimestamp": "2025-06-27T11:02:00Z" - }, - "spec": { - "description": "Display Angular warnings in dashboards and panels", - "stage": "GA", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "annotationPermissionUpdate", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-31T13:30:13Z" }, "spec": { @@ -534,7 +507,7 @@ { "metadata": { "name": "appPlatformGrpcClientAuth", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-10-14T10:47:18Z" }, "spec": { @@ -548,7 +521,7 @@ { "metadata": { "name": "assetSriChecks", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-04T10:56:35Z" }, "spec": { @@ -561,7 +534,7 @@ { "metadata": { "name": "authZGRPCServer", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-13T09:41:35Z" }, "spec": { @@ -575,7 +548,7 @@ { "metadata": { "name": "awsAsyncQueryCaching", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-07-21T15:34:07Z" }, "spec": { @@ -588,11 +561,8 @@ { "metadata": { "name": "awsDatasourcesTempCredentials", - "resourceVersion": "1752523189825", - "creationTimestamp": "2023-07-06T15:06:11Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-14 19:59:49.825959 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2023-07-06T15:06:11Z" }, "spec": { "description": "Support temporary security credentials in AWS plugins for Grafana Cloud customers", @@ -604,7 +574,7 @@ { "metadata": { "name": "azureMonitorDisableLogLimit", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-10-24T13:32:09Z" }, "spec": { @@ -617,7 +587,7 @@ { "metadata": { "name": "azureMonitorEnableUserAuth", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-27T14:01:54Z" }, "spec": { @@ -630,7 +600,7 @@ { "metadata": { "name": "azureMonitorLogsBuilderEditor", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-02T14:15:25Z" }, "spec": { @@ -643,7 +613,7 @@ { "metadata": { "name": "azureMonitorPrometheusExemplars", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-06T16:53:17Z" }, "spec": { @@ -656,7 +626,7 @@ { "metadata": { "name": "cachingOptimizeSerializationMemoryUsage", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-12T16:56:49Z" }, "spec": { @@ -668,7 +638,7 @@ { "metadata": { "name": "canvasPanelNesting", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-05-31T19:03:34Z" }, "spec": { @@ -682,7 +652,7 @@ { "metadata": { "name": "canvasPanelPanZoom", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-01-02T19:52:21Z" }, "spec": { @@ -695,7 +665,7 @@ { "metadata": { "name": "cloudRBACRoles", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-01-10T13:19:01Z" }, "spec": { @@ -711,7 +681,7 @@ { "metadata": { "name": "cloudWatchBatchQueries", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-20T19:09:41Z" }, "spec": { @@ -723,7 +693,7 @@ { "metadata": { "name": "cloudWatchCrossAccountQuerying", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-11-28T11:39:12Z" }, "spec": { @@ -737,7 +707,7 @@ { "metadata": { "name": "cloudWatchNewLabelParsing", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-04-05T15:57:56Z" }, "spec": { @@ -750,7 +720,7 @@ { "metadata": { "name": "cloudWatchRoundUpEndTime", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-27T15:10:28Z" }, "spec": { @@ -763,7 +733,7 @@ { "metadata": { "name": "configurableSchedulerTick", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-07-26T16:44:12Z" }, "spec": { @@ -777,7 +747,7 @@ { "metadata": { "name": "correlations", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-09-16T13:14:27Z" }, "spec": { @@ -791,7 +761,7 @@ { "metadata": { "name": "crashDetection", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-12T15:07:27Z" }, "spec": { @@ -804,7 +774,7 @@ { "metadata": { "name": "dashboardDisableSchemaValidationV1", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -816,7 +786,7 @@ { "metadata": { "name": "dashboardDisableSchemaValidationV2", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -828,8 +798,8 @@ { "metadata": { "name": "dashboardDsAdHocFiltering", - "resourceVersion": "1752487974435", - "creationTimestamp": "2025-07-14T10:12:54Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enables adhoc filtering support for the dashboard datasource", @@ -841,7 +811,7 @@ { "metadata": { "name": "dashboardNewLayouts", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-10-23T08:55:45Z" }, "spec": { @@ -854,7 +824,7 @@ { "metadata": { "name": "dashboardScene", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-13T08:51:21Z" }, "spec": { @@ -868,7 +838,7 @@ { "metadata": { "name": "dashboardSceneForViewers", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-02T19:02:25Z" }, "spec": { @@ -882,7 +852,7 @@ { "metadata": { "name": "dashboardSceneSolo", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-02-11T08:08:47Z" }, "spec": { @@ -896,7 +866,7 @@ { "metadata": { "name": "dashboardSchemaValidationLogging", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-11T16:52:46Z" }, "spec": { @@ -908,7 +878,7 @@ { "metadata": { "name": "dashgpt", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-08-30T20:22:05Z" }, "spec": { @@ -922,7 +892,7 @@ { "metadata": { "name": "dataplaneAggregator", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-08-09T08:41:07Z" }, "spec": { @@ -935,7 +905,7 @@ { "metadata": { "name": "dataplaneFrontendFallback", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-04-07T21:13:19Z" }, "spec": { @@ -950,7 +920,7 @@ { "metadata": { "name": "datasourceAPIServers", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-09-19T08:28:27Z" }, "spec": { @@ -963,7 +933,7 @@ { "metadata": { "name": "datasourceConnectionsTab", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-01-21T17:39:48Z" }, "spec": { @@ -976,7 +946,7 @@ { "metadata": { "name": "datasourceQueryTypes", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-05-23T16:46:28Z" }, "spec": { @@ -989,7 +959,7 @@ { "metadata": { "name": "disableClassicHTTPHistogram", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-18T19:37:44Z" }, "spec": { @@ -1003,7 +973,7 @@ { "metadata": { "name": "disableEnvelopeEncryption", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-05-24T08:34:47Z" }, "spec": { @@ -1017,7 +987,7 @@ { "metadata": { "name": "disableNumericMetricsSortingInExpressions", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-04-16T14:52:47Z" }, "spec": { @@ -1030,7 +1000,7 @@ { "metadata": { "name": "disableSSEDataplane", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-04-12T16:24:34Z" }, "spec": { @@ -1042,7 +1012,7 @@ { "metadata": { "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-01-24T09:43:44Z" }, "spec": { @@ -1055,7 +1025,7 @@ { "metadata": { "name": "elasticsearchCrossClusterSearch", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-12T22:20:04Z" }, "spec": { @@ -1067,7 +1037,7 @@ { "metadata": { "name": "elasticsearchImprovedParsing", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-01-15T17:05:54Z" }, "spec": { @@ -1079,25 +1049,8 @@ { "metadata": { "name": "enableAppChromeExtensions", - "resourceVersion": "1750235111726", - "creationTimestamp": "2025-06-18T08:25:11Z" - }, - "spec": { - "description": "Set this to true to enable all app chrome extensions registered by plugins.", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true, - "hideFromAdminPage": true, - "hideFromDocs": true, - "expression": "false" - } - }, - { - "metadata": { - "name": "enableAppChromeExtensions", - "resourceVersion": "1751899214406", - "creationTimestamp": "2025-07-07T14:40:14Z", - "deletionTimestamp": "2025-07-07T15:15:21Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Set this to true to enable all app chrome extensions registered by plugins.", @@ -1112,7 +1065,7 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-04-24T14:46:31Z" }, "spec": { @@ -1125,7 +1078,7 @@ { "metadata": { "name": "enableExtensionsAdminPage", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-05T15:55:10Z" }, "spec": { @@ -1138,7 +1091,7 @@ { "metadata": { "name": "enableNativeHTTPHistogram", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-03T18:23:55Z" }, "spec": { @@ -1152,8 +1105,8 @@ { "metadata": { "name": "enablePluginImporter", - "resourceVersion": "1752591728321", - "creationTimestamp": "2025-07-15T15:02:08Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Set this to true to use the new PluginImporter functionality", @@ -1168,11 +1121,8 @@ { "metadata": { "name": "enableSCIM", - "resourceVersion": "1750952338509", - "creationTimestamp": "2024-11-07T14:38:46Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-26 15:38:58.509301321 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2024-11-07T14:38:46Z" }, "spec": { "description": "Enables SCIM support for user and group management", @@ -1183,7 +1133,7 @@ { "metadata": { "name": "enableScopesInMetricsExplore", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-06T13:11:33Z" }, "spec": { @@ -1197,7 +1147,7 @@ { "metadata": { "name": "exploreLogsAggregatedMetrics", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1210,7 +1160,7 @@ { "metadata": { "name": "exploreLogsLimitedTimeRange", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1223,7 +1173,7 @@ { "metadata": { "name": "exploreLogsShardSplitting", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-08-29T13:55:59Z" }, "spec": { @@ -1236,7 +1186,7 @@ { "metadata": { "name": "exploreMetricsRelatedLogs", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-05T16:28:43Z" }, "spec": { @@ -1249,7 +1199,7 @@ { "metadata": { "name": "expressionParser", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-02-17T00:59:11Z" }, "spec": { @@ -1262,7 +1212,7 @@ { "metadata": { "name": "extensionSidebar", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-03T10:16:35Z" }, "spec": { @@ -1272,40 +1222,10 @@ "frontend": true } }, - { - "metadata": { - "name": "extensionsReadOnlyProxy", - "resourceVersion": "1750434297879", - "creationTimestamp": "2025-05-06T04:55:23Z", - "deletionTimestamp": "2025-06-30T08:24:11Z" - }, - "spec": { - "description": "Use proxy-based read-only objects for plugin extensions instead of deep cloning", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true, - "hideFromAdminPage": true, - "hideFromDocs": true - } - }, - { - "metadata": { - "name": "externalCorePlugins", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-09-22T08:50:13Z", - "deletionTimestamp": "2025-07-21T09:49:02Z" - }, - "spec": { - "description": "Allow core plugins to be loaded as external", - "stage": "GA", - "codeowner": "@grafana/plugins-platform-backend", - "expression": "true" - } - }, { "metadata": { "name": "externalServiceAccounts", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-09-28T07:26:37Z" }, "spec": { @@ -1318,7 +1238,7 @@ { "metadata": { "name": "extraThemes", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-05-10T13:37:04Z", "deletionTimestamp": "2025-05-20T08:18:08Z" }, @@ -1332,7 +1252,7 @@ { "metadata": { "name": "extractFieldsNameDeduplication", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-02T15:47:42Z" }, "spec": { @@ -1342,24 +1262,10 @@ "frontend": true } }, - { - "metadata": { - "name": "failWrongDSUID", - "resourceVersion": "1750434297879", - "creationTimestamp": "2024-06-20T10:56:39Z", - "deletionTimestamp": "2025-06-27T13:43:58Z" - }, - "spec": { - "description": "Throws an error if a data source has an invalid UIDs", - "stage": "GA", - "codeowner": "@grafana/plugins-platform-backend", - "expression": "true" - } - }, { "metadata": { "name": "faroDatasourceSelector", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-05-05T00:35:10Z" }, "spec": { @@ -1372,7 +1278,7 @@ { "metadata": { "name": "featureHighlights", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-02-03T11:53:23Z" }, "spec": { @@ -1386,7 +1292,7 @@ { "metadata": { "name": "featureToggleAdminPage", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-07-18T20:43:32Z" }, "spec": { @@ -1400,7 +1306,7 @@ { "metadata": { "name": "feedbackButton", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-02T17:08:15Z" }, "spec": { @@ -1413,7 +1319,7 @@ { "metadata": { "name": "fetchRulesUsingPost", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-01-29T12:17:44Z" }, "spec": { @@ -1427,8 +1333,8 @@ { "metadata": { "name": "foldersAppPlatformAPI", - "resourceVersion": "1751377081192", - "creationTimestamp": "2025-07-01T13:38:01Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enables use of app platform API for folders", @@ -1443,7 +1349,7 @@ { "metadata": { "name": "formatString", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-13T18:17:12Z" }, "spec": { @@ -1457,7 +1363,7 @@ { "metadata": { "name": "grafanaAPIServerEnsureKubectlAccess", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-12-06T20:21:21Z" }, "spec": { @@ -1471,7 +1377,7 @@ { "metadata": { "name": "grafanaAPIServerWithExperimentalAPIs", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-06T18:55:22Z" }, "spec": { @@ -1485,7 +1391,7 @@ { "metadata": { "name": "grafanaAdvisor", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-01-20T10:08:00Z" }, "spec": { @@ -1497,7 +1403,7 @@ { "metadata": { "name": "grafanaManagedRecordingRules", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-04-22T17:53:16Z", "deletionTimestamp": "2025-05-19T10:15:49Z" }, @@ -1512,7 +1418,7 @@ { "metadata": { "name": "grafanaconThemes", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-02-06T11:08:04Z" }, "spec": { @@ -1528,7 +1434,7 @@ { "metadata": { "name": "groupAttributeSync", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-09-09T15:29:43Z" }, "spec": { @@ -1541,7 +1447,7 @@ { "metadata": { "name": "groupByVariable", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-02-14T17:18:04Z" }, "spec": { @@ -1555,7 +1461,7 @@ { "metadata": { "name": "groupToNestedTableTransformation", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-02-07T14:28:26Z" }, "spec": { @@ -1569,7 +1475,7 @@ { "metadata": { "name": "grpcServer", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-09-26T20:25:34Z" }, "spec": { @@ -1582,11 +1488,8 @@ { "metadata": { "name": "improvedExternalSessionHandling", - "resourceVersion": "1751355094344", - "creationTimestamp": "2024-09-17T10:54:39Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-01 07:31:34.344238 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2024-09-17T10:54:39Z" }, "spec": { "description": "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.", @@ -1599,11 +1502,8 @@ { "metadata": { "name": "improvedExternalSessionHandlingSAML", - "resourceVersion": "1751355094344", - "creationTimestamp": "2025-01-09T17:02:49Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-01 07:31:34.344238 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-01-09T17:02:49Z" }, "spec": { "description": "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.", @@ -1616,7 +1516,7 @@ { "metadata": { "name": "individualCookiePreferences", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-02-21T10:19:07Z" }, "spec": { @@ -1628,7 +1528,7 @@ { "metadata": { "name": "infinityRunQueriesInParallel", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-14T12:54:04Z" }, "spec": { @@ -1640,7 +1540,7 @@ { "metadata": { "name": "influxdbBackendMigration", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-02-09T18:26:16Z", "deletionTimestamp": "2023-01-17T14:11:26Z" }, @@ -1655,7 +1555,7 @@ { "metadata": { "name": "influxdbRunQueriesInParallel", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-02-01T10:58:24Z" }, "spec": { @@ -1667,7 +1567,7 @@ { "metadata": { "name": "influxqlStreamingParser", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-29T17:29:35Z" }, "spec": { @@ -1679,7 +1579,7 @@ { "metadata": { "name": "investigationsBackend", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-18T08:31:03Z" }, "spec": { @@ -1692,7 +1592,7 @@ { "metadata": { "name": "inviteUserExperimental", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-07T19:09:59Z" }, "spec": { @@ -1704,27 +1604,10 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "jaegerBackendMigration", - "resourceVersion": "1751465665226", - "creationTimestamp": "2024-11-15T14:40:20Z", - "deletionTimestamp": "2025-07-07T14:12:35Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-02 14:14:25.226989 +0000 UTC" - } - }, - "spec": { - "description": "Enables querying the Jaeger data source without the proxy", - "stage": "GA", - "codeowner": "@grafana/oss-big-tent", - "expression": "true" - } - }, { "metadata": { "name": "jitterAlertRulesWithinGroups", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-01-18T18:48:11Z" }, "spec": { @@ -1738,7 +1621,7 @@ { "metadata": { "name": "k8SFolderCounts", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -1751,7 +1634,7 @@ { "metadata": { "name": "k8SFolderMove", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-27T17:10:44Z" }, "spec": { @@ -1764,7 +1647,7 @@ { "metadata": { "name": "kubernetesAggregator", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-02-12T20:59:35Z" }, "spec": { @@ -1777,7 +1660,7 @@ { "metadata": { "name": "kubernetesAggregatorCapTokenAuth", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-05-15T18:14:23Z" }, "spec": { @@ -1790,7 +1673,7 @@ { "metadata": { "name": "kubernetesAuthzApis", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-06-18T07:43:01Z" }, "spec": { @@ -1804,7 +1687,7 @@ { "metadata": { "name": "kubernetesClientDashboardsFolders", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-02-18T23:11:26Z" }, "spec": { @@ -1817,7 +1700,7 @@ { "metadata": { "name": "kubernetesDashboards", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-05T14:34:23Z" }, "spec": { @@ -1830,7 +1713,7 @@ { "metadata": { "name": "kubernetesFeatureToggles", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-01-18T05:32:44Z" }, "spec": { @@ -1844,8 +1727,8 @@ { "metadata": { "name": "kubernetesLibraryPanelConnections", - "resourceVersion": "1753100797468", - "creationTimestamp": "2025-07-21T12:26:37Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Routes library panel connections requests from /api to using search", @@ -1857,7 +1740,7 @@ { "metadata": { "name": "kubernetesLibraryPanels", - "resourceVersion": "1750714411267", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-06-25T22:21:56Z" }, "spec": { @@ -1870,7 +1753,7 @@ { "metadata": { "name": "kubernetesSnapshots", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-12-05T22:31:49Z" }, "spec": { @@ -1883,11 +1766,8 @@ { "metadata": { "name": "localeFormatPreference", - "resourceVersion": "1753261256848", - "creationTimestamp": "2025-03-31T13:59:07Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-23 09:00:56.848352786 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-03-31T13:59:07Z" }, "spec": { "description": "Specifies the locale so the correct format for numbers and dates can be shown", @@ -1898,7 +1778,7 @@ { "metadata": { "name": "localizationForPlugins", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-31T04:38:38Z" }, "spec": { @@ -1910,7 +1790,7 @@ { "metadata": { "name": "logQLScope", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-11T11:53:24Z" }, "spec": { @@ -1925,7 +1805,7 @@ { "metadata": { "name": "logRequestsInstrumentedAsUnknown", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-06-10T08:56:55Z" }, "spec": { @@ -1937,7 +1817,7 @@ { "metadata": { "name": "logRowsPopoverMenu", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-16T09:48:10Z" }, "spec": { @@ -1951,7 +1831,7 @@ { "metadata": { "name": "logsContextDatasourceUi", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-01-27T14:12:01Z" }, "spec": { @@ -1966,7 +1846,7 @@ { "metadata": { "name": "logsExploreTableDefaultVisualization", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-05-02T15:28:15Z" }, "spec": { @@ -1979,7 +1859,7 @@ { "metadata": { "name": "logsExploreTableVisualisation", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-07-12T13:52:42Z" }, "spec": { @@ -1993,7 +1873,7 @@ { "metadata": { "name": "logsInfiniteScrolling", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-09T10:54:03Z" }, "spec": { @@ -2007,7 +1887,7 @@ { "metadata": { "name": "logsPanelControls", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-07T14:38:55Z" }, "spec": { @@ -2021,7 +1901,7 @@ { "metadata": { "name": "lokiExperimentalStreaming", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-06-19T10:03:51Z" }, "spec": { @@ -2033,7 +1913,7 @@ { "metadata": { "name": "lokiLabelNamesQueryApi", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-13T14:31:41Z" }, "spec": { @@ -2046,7 +1926,7 @@ { "metadata": { "name": "lokiLogsDataplane", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-07-13T07:58:00Z" }, "spec": { @@ -2055,39 +1935,10 @@ "codeowner": "@grafana/observability-logs" } }, - { - "metadata": { - "name": "lokiPredefinedOperations", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-06-02T10:52:36Z", - "deletionTimestamp": "2025-06-27T08:08:44Z" - }, - "spec": { - "description": "Adds predefined query operations to Loki query editor", - "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true - } - }, - { - "metadata": { - "name": "lokiQueryHints", - "resourceVersion": "1743693517832", - "creationTimestamp": "2023-12-18T20:43:16Z", - "deletionTimestamp": "2025-06-12T12:14:47Z" - }, - "spec": { - "description": "Enables query hints for Loki", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "frontend": true, - "expression": "true" - } - }, { "metadata": { "name": "lokiQuerySplitting", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-02-09T17:27:02Z" }, "spec": { @@ -2099,24 +1950,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "lokiQuerySplittingConfig", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-03-20T15:51:36Z", - "deletionTimestamp": "2025-06-27T09:32:43Z" - }, - "spec": { - "description": "Give users the option to configure split durations for Loki queries", - "stage": "experimental", - "codeowner": "@grafana/observability-logs", - "frontend": true - } - }, { "metadata": { "name": "lokiRunQueriesInParallel", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-09-19T09:34:01Z" }, "spec": { @@ -2128,7 +1965,7 @@ { "metadata": { "name": "lokiShardSplitting", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-10-23T11:21:03Z" }, "spec": { @@ -2138,24 +1975,10 @@ "frontend": true } }, - { - "metadata": { - "name": "lokiStructuredMetadata", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-11-16T16:06:14Z", - "deletionTimestamp": "2025-06-27T09:00:53Z" - }, - "spec": { - "description": "Enables the loki data source to request structured metadata from the Loki server", - "stage": "GA", - "codeowner": "@grafana/observability-logs", - "expression": "true" - } - }, { "metadata": { "name": "managedDualWriter", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-02-19T14:50:39Z" }, "spec": { @@ -2169,7 +1992,7 @@ { "metadata": { "name": "metricsFromProfiles", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-09T10:55:28Z" }, "spec": { @@ -2182,7 +2005,7 @@ { "metadata": { "name": "mlExpressions", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-07-13T17:37:50Z" }, "spec": { @@ -2194,7 +2017,7 @@ { "metadata": { "name": "multiTenantFrontend", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-25T09:24:25Z" }, "spec": { @@ -2206,11 +2029,8 @@ { "metadata": { "name": "multiTenantTempCredentials", - "resourceVersion": "1752523189825", - "creationTimestamp": "2025-04-02T20:25:50Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-14 19:59:49.825959 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-04-02T20:25:50Z" }, "spec": { "description": "use multi-tenant path for awsTempCredentials", @@ -2222,7 +2042,7 @@ { "metadata": { "name": "mysqlAnsiQuotes", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-10-12T11:43:35Z" }, "spec": { @@ -2231,24 +2051,10 @@ "codeowner": "@grafana/search-and-storage" } }, - { - "metadata": { - "name": "nanoGit", - "resourceVersion": "1750434297879", - "creationTimestamp": "2025-06-17T17:07:30Z", - "deletionTimestamp": "2025-07-09T11:53:17Z" - }, - "spec": { - "description": "Use experimental git library for provisioning", - "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", - "requiresRestart": true - } - }, { "metadata": { "name": "nestedFolders", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-10-26T14:15:14Z" }, "spec": { @@ -2261,7 +2067,7 @@ { "metadata": { "name": "newDashboardSharingComponent", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-05-03T15:02:18Z" }, "spec": { @@ -2275,7 +2081,7 @@ { "metadata": { "name": "newDashboardWithFiltersAndGroupBy", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-04-04T11:25:21Z" }, "spec": { @@ -2289,7 +2095,7 @@ { "metadata": { "name": "newFiltersUI", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-08-30T12:48:13Z" }, "spec": { @@ -2302,11 +2108,8 @@ { "metadata": { "name": "newInfluxDSConfigPageDesign", - "resourceVersion": "1750787021637", - "creationTimestamp": "2025-06-25T16:39:54Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-24 17:43:41.637607 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-06-25T16:39:54Z" }, "spec": { "description": "Enables new design for the InfluxDB data source configuration page", @@ -2318,7 +2121,7 @@ { "metadata": { "name": "newLogsPanel", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-02-04T17:40:17Z" }, "spec": { @@ -2331,11 +2134,8 @@ { "metadata": { "name": "newPDFRendering", - "resourceVersion": "1750821734038", - "creationTimestamp": "2024-02-08T12:09:34Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 03:22:14.038814 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2024-02-08T12:09:34Z" }, "spec": { "description": "New implementation for the dashboard-to-PDF rendering", @@ -2347,11 +2147,8 @@ { "metadata": { "name": "newShareReportDrawer", - "resourceVersion": "1750821734038", - "creationTimestamp": "2025-02-17T19:05:46Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 03:22:14.038814 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-02-17T19:05:46Z" }, "spec": { "description": "Enables the report creation drawer in a dashboard", @@ -2364,7 +2161,7 @@ { "metadata": { "name": "oauthRequireSubClaim", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-03-25T13:22:24Z" }, "spec": { @@ -2378,7 +2175,7 @@ { "metadata": { "name": "onPremToCloudMigrations", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-01-22T16:09:08Z" }, "spec": { @@ -2391,8 +2188,8 @@ { "metadata": { "name": "otelLogsFormatting", - "resourceVersion": "1750152612973", - "creationTimestamp": "2025-06-17T09:30:12Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Applies OTel formatting templates to displayed logs", @@ -2404,7 +2201,7 @@ { "metadata": { "name": "panelFilterVariable", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-03T12:15:54Z" }, "spec": { @@ -2418,7 +2215,7 @@ { "metadata": { "name": "panelMonitoring", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-10-09T05:19:08Z" }, "spec": { @@ -2432,7 +2229,7 @@ { "metadata": { "name": "panelTitleSearch", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-02-15T18:26:03Z" }, "spec": { @@ -2445,7 +2242,7 @@ { "metadata": { "name": "passwordlessMagicLinkAuthentication", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-14T13:50:55Z" }, "spec": { @@ -2459,11 +2256,8 @@ { "metadata": { "name": "pdfTables", - "resourceVersion": "1750821734038", - "creationTimestamp": "2023-11-06T13:39:22Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 03:22:14.038814 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2023-11-06T13:39:22Z" }, "spec": { "description": "Enables generating table data as PDF in reporting", @@ -2474,7 +2268,7 @@ { "metadata": { "name": "permissionsFilterRemoveSubquery", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-08-02T07:39:25Z" }, "spec": { @@ -2486,7 +2280,7 @@ { "metadata": { "name": "pinNavItems", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-10T11:40:03Z" }, "spec": { @@ -2499,7 +2293,7 @@ { "metadata": { "name": "playlistsReconciler", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-20T03:09:31Z" }, "spec": { @@ -2512,8 +2306,8 @@ { "metadata": { "name": "pluginAssetProvider", - "resourceVersion": "1752486584712", - "creationTimestamp": "2025-07-14T09:49:44Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Allows decoupled core plugins to load from the Grafana CDN", @@ -2525,27 +2319,10 @@ "expression": "false" } }, - { - "metadata": { - "name": "pluginLoadingRefactor", - "resourceVersion": "1752218524617", - "creationTimestamp": "2025-07-11T07:22:04Z", - "deletionTimestamp": "2025-07-11T07:23:34Z" - }, - "spec": { - "description": "Set this to true to use the new plugin loading functionality", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend", - "frontend": true, - "hideFromAdminPage": true, - "hideFromDocs": true, - "expression": "false" - } - }, { "metadata": { "name": "pluginProxyPreserveTrailingSlash", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-05T11:36:14Z" }, "spec": { @@ -2558,7 +2335,7 @@ { "metadata": { "name": "pluginsAutoUpdate", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-16T11:44:39Z" }, "spec": { @@ -2567,23 +2344,10 @@ "codeowner": "@grafana/plugins-platform-backend" } }, - { - "metadata": { - "name": "pluginsCDNSyncLoader", - "resourceVersion": "1750434297879", - "creationTimestamp": "2025-02-07T10:07:08Z", - "deletionTimestamp": "2025-07-21T10:13:07Z" - }, - "spec": { - "description": "Loads plugins from CDN synchronously", - "stage": "experimental", - "codeowner": "@grafana/plugins-platform-backend" - } - }, { "metadata": { "name": "pluginsDetailsRightPanel", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-08-13T09:55:30Z" }, "spec": { @@ -2597,7 +2361,7 @@ { "metadata": { "name": "pluginsFrontendSandbox", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-06-05T08:51:36Z" }, "spec": { @@ -2609,7 +2373,7 @@ { "metadata": { "name": "pluginsSkipHostEnvVars", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-11-15T17:09:14Z" }, "spec": { @@ -2621,7 +2385,7 @@ { "metadata": { "name": "pluginsSriChecks", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-10-04T12:55:09Z" }, "spec": { @@ -2634,7 +2398,7 @@ { "metadata": { "name": "preferLibraryPanelTitle", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-06-17T11:21:21Z" }, "spec": { @@ -2647,7 +2411,7 @@ { "metadata": { "name": "preinstallAutoUpdate", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-07T12:14:25Z" }, "spec": { @@ -2660,7 +2424,7 @@ { "metadata": { "name": "preserveDashboardStateWhenNavigating", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-05-27T12:28:06Z" }, "spec": { @@ -2674,7 +2438,7 @@ { "metadata": { "name": "promQLScope", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-01-29T20:22:17Z" }, "spec": { @@ -2689,7 +2453,7 @@ { "metadata": { "name": "prometheusAzureOverrideAudience", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-05-30T15:43:32Z", "deletionTimestamp": "2023-07-16T21:30:14Z" }, @@ -2703,7 +2467,7 @@ { "metadata": { "name": "prometheusCodeModeMetricNamesSearch", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-04-04T20:38:23Z" }, "spec": { @@ -2716,7 +2480,7 @@ { "metadata": { "name": "prometheusSpecialCharsInLabelValues", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-18T21:31:08Z" }, "spec": { @@ -2729,7 +2493,7 @@ { "metadata": { "name": "provisioning", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-22T09:03:50Z" }, "spec": { @@ -2742,11 +2506,8 @@ { "metadata": { "name": "provisioningSecretsService", - "resourceVersion": "1752501981893", - "creationTimestamp": "2025-07-14T14:05:50Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-14 14:06:21.89322 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Experimental feature to use the secrets service for provisioning instead of the legacy secrets", @@ -2758,11 +2519,8 @@ { "metadata": { "name": "publicDashboardsEmailSharing", - "resourceVersion": "1750821734038", - "creationTimestamp": "2023-01-03T19:45:15Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 03:22:14.038814 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2023-01-03T19:45:15Z" }, "spec": { "description": "Enables public dashboard sharing to be restricted to only allowed emails", @@ -2775,11 +2533,8 @@ { "metadata": { "name": "publicDashboardsScene", - "resourceVersion": "1750821734038", - "creationTimestamp": "2024-03-22T14:48:21Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 03:22:14.038814 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2024-03-22T14:48:21Z" }, "spec": { "description": "Enables public dashboard rendering using scenes", @@ -2792,12 +2547,9 @@ { "metadata": { "name": "queryLibrary", - "resourceVersion": "1750876431546", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-10-07T18:31:45Z", - "deletionTimestamp": "2023-03-20T16:00:14Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 18:33:51.546111 +0000 UTC" - } + "deletionTimestamp": "2023-03-20T16:00:14Z" }, "spec": { "description": "Enables Query Library feature in Explore", @@ -2808,7 +2560,7 @@ { "metadata": { "name": "queryService", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2821,7 +2573,7 @@ { "metadata": { "name": "queryServiceFromExplore", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-02T10:00:33Z" }, "spec": { @@ -2834,7 +2586,7 @@ { "metadata": { "name": "queryServiceFromUI", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2847,7 +2599,7 @@ { "metadata": { "name": "queryServiceRewrite", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-04-19T09:26:21Z" }, "spec": { @@ -2860,7 +2612,7 @@ { "metadata": { "name": "recordedQueriesMulti", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-06-14T12:34:22Z" }, "spec": { @@ -2873,7 +2625,7 @@ { "metadata": { "name": "refactorVariablesTimeRange", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-06-06T13:12:09Z" }, "spec": { @@ -2886,9 +2638,8 @@ { "metadata": { "name": "regressionTransformation", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-11-24T14:49:16Z", - "deletionTimestamp": "2025-07-01T13:24:02Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2023-11-24T14:49:16Z" }, "spec": { "description": "Enables regression analysis transformation", @@ -2900,7 +2651,7 @@ { "metadata": { "name": "reloadDashboardsOnParamsChange", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-10-25T12:56:54Z" }, "spec": { @@ -2914,11 +2665,8 @@ { "metadata": { "name": "renderAuthJWT", - "resourceVersion": "1750821734038", - "creationTimestamp": "2023-04-03T16:53:38Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 03:22:14.038814 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2023-04-03T16:53:38Z" }, "spec": { "description": "Uses JWT-based auth for rendering instead of relying on remote cache", @@ -2930,11 +2678,8 @@ { "metadata": { "name": "rendererDisableAppPluginsPreload", - "resourceVersion": "1750821734038", - "creationTimestamp": "2025-02-24T14:43:06Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 03:22:14.038814 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-02-24T14:43:06Z" }, "spec": { "description": "Disable pre-loading app plugins when the request is coming from the renderer", @@ -2948,11 +2693,8 @@ { "metadata": { "name": "reportingRetries", - "resourceVersion": "1750821734038", - "creationTimestamp": "2023-08-31T07:47:47Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-06-25 03:22:14.038814 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2023-08-31T07:47:47Z" }, "spec": { "description": "Enables rendering retries for the reporting feature", @@ -2964,7 +2706,7 @@ { "metadata": { "name": "restoreDashboards", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-05-23T14:35:54Z" }, "spec": { @@ -2978,7 +2720,7 @@ { "metadata": { "name": "rolePickerDrawer", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-09-26T12:51:38Z" }, "spec": { @@ -2990,7 +2732,7 @@ { "metadata": { "name": "scopeApi", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-11-27T07:58:25Z" }, "spec": { @@ -3004,7 +2746,7 @@ { "metadata": { "name": "scopeFilters", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-03-05T15:41:19Z" }, "spec": { @@ -3018,7 +2760,7 @@ { "metadata": { "name": "scopeSearchAllLevels", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-14T07:42:16Z" }, "spec": { @@ -3032,7 +2774,7 @@ { "metadata": { "name": "secretsManagementAppPlatform", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-19T09:25:14Z" }, "spec": { @@ -3044,8 +2786,8 @@ { "metadata": { "name": "sharingDashboardImage", - "resourceVersion": "1747090555040", - "creationTimestamp": "2025-05-12T22:55:55Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enables image sharing functionality for dashboards", @@ -3058,7 +2800,7 @@ { "metadata": { "name": "showDashboardValidationWarnings", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-10-14T13:51:05Z" }, "spec": { @@ -3070,11 +2812,8 @@ { "metadata": { "name": "skipTokenRotationIfRecent", - "resourceVersion": "1751872762065", - "creationTimestamp": "2025-06-03T06:59:40Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-07 07:19:22.065046 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-06-03T06:59:40Z" }, "spec": { "description": "Skip token rotation if it was already rotated less than 5 seconds ago", @@ -3088,7 +2827,7 @@ { "metadata": { "name": "sqlDatasourceDatabaseSelection", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-06-06T16:28:52Z" }, "spec": { @@ -3102,7 +2841,7 @@ { "metadata": { "name": "sqlExpressions", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-02-27T21:16:00Z" }, "spec": { @@ -3127,7 +2866,7 @@ { "metadata": { "name": "sseGroupByDatasource", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-09-07T20:02:07Z" }, "spec": { @@ -3136,25 +2875,10 @@ "codeowner": "@grafana/observability-metrics" } }, - { - "metadata": { - "name": "ssoSettingsApi", - "resourceVersion": "1750434297879", - "creationTimestamp": "2023-11-08T09:50:01Z", - "deletionTimestamp": "2025-07-02T14:16:57Z" - }, - "spec": { - "description": "Enables the SSO settings API and the OAuth configuration UIs in Grafana", - "stage": "GA", - "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true, - "expression": "true" - } - }, { "metadata": { "name": "ssoSettingsLDAP", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-18T11:31:27Z" }, "spec": { @@ -3166,25 +2890,10 @@ "expression": "true" } }, - { - "metadata": { - "name": "ssoSettingsSAML", - "resourceVersion": "1750434297879", - "creationTimestamp": "2024-03-14T11:04:45Z", - "deletionTimestamp": "2025-07-15T09:43:39Z" - }, - "spec": { - "description": "Use the new SSO Settings API to configure the SAML connector", - "stage": "GA", - "codeowner": "@grafana/identity-access-team", - "allowSelfServe": true, - "expression": "true" - } - }, { "metadata": { "name": "storage", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2022-03-17T17:19:23Z" }, "spec": { @@ -3196,7 +2905,7 @@ { "metadata": { "name": "tableNextGen", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-26T03:57:57Z" }, "spec": { @@ -3209,7 +2918,7 @@ { "metadata": { "name": "tableSharedCrosshair", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-12-13T09:33:14Z" }, "spec": { @@ -3222,11 +2931,8 @@ { "metadata": { "name": "tabularNumbers", - "resourceVersion": "1752078306230", - "creationTimestamp": "2025-06-24T11:52:03Z", - "annotations": { - "grafana.app/updatedTimestamp": "2025-07-09 16:25:06.230922635 +0000 UTC" - } + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-06-24T11:52:03Z" }, "spec": { "description": "Use fixed-width numbers globally in the UI", @@ -3238,7 +2944,7 @@ { "metadata": { "name": "teamHttpHeadersMimir", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-01-13T10:42:47Z" }, "spec": { @@ -3252,7 +2958,7 @@ { "metadata": { "name": "teamHttpHeadersTempo", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-05-22T19:13:31Z" }, "spec": { @@ -3264,7 +2970,7 @@ { "metadata": { "name": "templateVariablesUsesCombobox", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-01-31T09:53:13Z" }, "spec": { @@ -3277,8 +2983,8 @@ { "metadata": { "name": "tempoAlerting", - "resourceVersion": "1750158458662", - "creationTimestamp": "2025-06-17T11:07:38Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enables creating alerts from Tempo data source", @@ -3287,10 +2993,23 @@ "frontend": true } }, + { + "metadata": { + "name": "timeComparison", + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" + }, + "spec": { + "description": "Enables time comparison option in supported panels", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, { "metadata": { "name": "timeRangeProvider", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-10-22T10:52:33Z" }, "spec": { @@ -3302,7 +3021,7 @@ { "metadata": { "name": "tlsMemcached", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-05-09T19:12:08Z" }, "spec": { @@ -3315,7 +3034,7 @@ { "metadata": { "name": "transformationsRedesign", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-07-12T16:35:49Z" }, "spec": { @@ -3330,7 +3049,7 @@ { "metadata": { "name": "unifiedHistory", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-13T10:41:18Z" }, "spec": { @@ -3343,7 +3062,7 @@ { "metadata": { "name": "unifiedNavbars", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-04-09T12:51:22Z" }, "spec": { @@ -3357,7 +3076,7 @@ { "metadata": { "name": "unifiedRequestLog", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2023-03-31T13:38:09Z" }, "spec": { @@ -3371,7 +3090,7 @@ { "metadata": { "name": "unifiedStorageBigObjectsSupport", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-10-17T10:18:29Z" }, "spec": { @@ -3383,7 +3102,7 @@ { "metadata": { "name": "unifiedStorageGrpcConnectionPool", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-21T13:24:54Z" }, "spec": { @@ -3397,7 +3116,7 @@ { "metadata": { "name": "unifiedStorageHistoryPruner", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-17T10:36:38Z" }, "spec": { @@ -3412,7 +3131,7 @@ { "metadata": { "name": "unifiedStorageSearch", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-09-30T19:46:14Z" }, "spec": { @@ -3426,8 +3145,8 @@ { "metadata": { "name": "unifiedStorageSearchDualReaderEnabled", - "resourceVersion": "1752500336818", - "creationTimestamp": "2025-07-14T13:38:56Z" + "resourceVersion": "1753285129398", + "creationTimestamp": "2025-07-23T15:38:49Z" }, "spec": { "description": "Enable dual reader for unified storage search", @@ -3440,7 +3159,7 @@ { "metadata": { "name": "unifiedStorageSearchPermissionFiltering", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-01-22T11:38:37Z" }, "spec": { @@ -3455,7 +3174,7 @@ { "metadata": { "name": "unifiedStorageSearchSprinkles", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-18T17:00:54Z" }, "spec": { @@ -3469,7 +3188,7 @@ { "metadata": { "name": "unifiedStorageSearchUI", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-12-19T18:21:48Z" }, "spec": { @@ -3483,7 +3202,7 @@ { "metadata": { "name": "useScopesNavigationEndpoint", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2025-03-31T15:20:00Z" }, "spec": { @@ -3498,7 +3217,7 @@ { "metadata": { "name": "useSessionStorageForRedirection", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-09-23T09:31:23Z" }, "spec": { @@ -3511,7 +3230,7 @@ { "metadata": { "name": "zanzana", - "resourceVersion": "1750434297879", + "resourceVersion": "1753285129398", "creationTimestamp": "2024-06-19T13:59:47Z" }, "spec": { diff --git a/public/app/features/dashboard-scene/scene/CustomTimeRangeCompare.tsx b/public/app/features/dashboard-scene/scene/CustomTimeRangeCompare.tsx new file mode 100644 index 00000000000..0e0055a2e73 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/CustomTimeRangeCompare.tsx @@ -0,0 +1,60 @@ +import { SceneTimeRangeCompare, SceneComponentProps, VizPanel, sceneGraph } from '@grafana/scenes'; +import { TimeCompareOptions } from '@grafana/schema'; + +function hasTimeCompare(options: unknown): options is TimeCompareOptions { + return options != null && typeof options === 'object' && 'timeCompare' in options; +} + +export class CustomTimeRangeCompare extends SceneTimeRangeCompare { + constructor(state: Partial = {}) { + super({ + ...state, + compareWith: undefined, + compareOptions: [], + hideCheckbox: true, + }); + + this.addActivationHandler(() => this._activationHandler()); + } + + private _activationHandler() { + // Subscribe to parent panel's options changes + const vizPanel = sceneGraph.getAncestor(this, VizPanel); + + this._subs.add( + vizPanel.subscribeToState((newState, prevState) => { + const newTimeCompareEnabled = hasTimeCompare(newState.options) && newState.options.timeCompare; + const prevTimeCompareEnabled = hasTimeCompare(prevState.options) && prevState.options.timeCompare; + + // Only act when transitioning from enabled to disabled + if (prevTimeCompareEnabled && !newTimeCompareEnabled) { + this._handleDisable(); + } + }) + ); + } + + private _handleDisable() { + // Only clear state if there's actually a comparison active + if (this.state.compareWith) { + this.setState({ + compareWith: undefined, + }); + } + } + + static Component = function CustomTimeRangeCompareRenderer({ model }: SceneComponentProps) { + // Get the parent VizPanel to check timeCompare option + const vizPanel = sceneGraph.getAncestor(model, VizPanel); + const { options } = vizPanel.useState(); + + // Check if timeCompare is enabled + const isTimeCompareEnabled = hasTimeCompare(options) && options.timeCompare; + + if (!isTimeCompareEnabled) { + return <>; + } + + return ; + }; +} diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts index b5f9950b5f6..fce584cf4f7 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.test.ts @@ -1,6 +1,8 @@ -import { PanelQueryKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; +import { PanelQueryKind, PanelKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; -import { getRuntimePanelDataSource } from './utils'; +import { CustomTimeRangeCompare } from '../../scene/CustomTimeRangeCompare'; + +import { buildVizPanel, getRuntimePanelDataSource } from './utils'; // Mock the config needed for the function jest.mock('@grafana/runtime', () => ({ @@ -32,9 +34,81 @@ jest.mock('@grafana/runtime', () => ({ }, }, }, + featureToggles: { + timeComparison: false, + }, }, })); +// Mock only what's essential for header actions tests +jest.mock('../../scene/CustomTimeRangeCompare', () => ({ + CustomTimeRangeCompare: jest.fn(), +})); + +// Helper function to create a minimal panel for testing +const createTestPanel = (): PanelKind => ({ + kind: 'Panel', + spec: { + id: 1, + title: 'Test Panel', + description: '', + vizConfig: { + kind: 'timeseries', + spec: { + options: {}, + fieldConfig: { defaults: {}, overrides: [] }, + pluginVersion: '1.0.0', + }, + }, + data: { + kind: 'QueryGroup', + spec: { + queries: [], + queryOptions: {}, + transformations: [], + }, + }, + links: [], + }, +}); + +describe('buildVizPanel', () => { + describe('header actions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should include CustomTimeRangeCompare in headerActions when timeComparison feature toggle is enabled', () => { + // Mock config with timeComparison enabled + const mockConfig = require('@grafana/runtime').config; + mockConfig.featureToggles.timeComparison = true; + + const panel = createTestPanel(); + const vizPanel = buildVizPanel(panel); + + expect(vizPanel.state.headerActions).toBeDefined(); + expect(vizPanel.state.headerActions).toHaveLength(1); + expect(CustomTimeRangeCompare).toHaveBeenCalledWith({ + key: 'time-compare', + compareWith: undefined, + compareOptions: [], + }); + }); + + it('should not include headerActions when timeComparison feature toggle is disabled', () => { + // Mock config with timeComparison disabled + const mockConfig = require('@grafana/runtime').config; + mockConfig.featureToggles.timeComparison = false; + + const panel = createTestPanel(); + const vizPanel = buildVizPanel(panel); + + expect(vizPanel.state.headerActions).toBeUndefined(); + expect(CustomTimeRangeCompare).not.toHaveBeenCalled(); + }); + }); +}); + describe('getRuntimePanelDataSource', () => { it('should return the datasource when it is specified in the query', () => { const query: PanelQueryKind = { diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index c8018a98be0..b3b3325ab21 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -25,6 +25,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering'; import { ConditionalRenderingGroup } from '../../conditional-rendering/ConditionalRenderingGroup'; import { conditionalRenderingSerializerRegistry } from '../../conditional-rendering/serializers'; +import { CustomTimeRangeCompare } from '../../scene/CustomTimeRangeCompare'; import { DashboardDatasourceBehaviour } from '../../scene/DashboardDatasourceBehaviour'; import { DashboardScene } from '../../scene/DashboardScene'; import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior'; @@ -73,6 +74,9 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel { $behaviors: [], extendPanelContext: setDashboardPanelContext, // _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), //FIXME: Angular Migration + headerActions: config.featureToggles.timeComparison + ? [new CustomTimeRangeCompare({ key: 'time-compare', compareWith: undefined, compareOptions: [] })] + : undefined, }; if (!config.publicDashboardAccessToken) { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index ef03bdd17ee..7e9302634cd 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -810,6 +810,41 @@ describe('transformSaveModelToScene', () => { expect((libPanelBehavior as LibraryPanelBehavior).state.name).toEqual(panel.libraryPanel.name); expect(gridItem.state.body.state.title).toEqual(panel.title); }); + + describe('header actions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should include headerActions when timeComparison feature toggle is enabled', () => { + config.featureToggles.timeComparison = true; + + const panel = { + title: 'Test Panel', + type: 'timeseries', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + }; + + const { vizPanel } = buildGridItemForTest(panel); + + expect(vizPanel.state.headerActions).toBeDefined(); + expect(vizPanel.state.headerActions).toHaveLength(1); + }); + + it('should not include headerActions when timeComparison feature toggle is disabled', () => { + config.featureToggles.timeComparison = false; + + const panel = { + title: 'Test Panel', + type: 'timeseries', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + }; + + const { vizPanel } = buildGridItemForTest(panel); + + expect(vizPanel.state.headerActions).toBeUndefined(); + }); + }); }); describe('Convert to new rows', () => { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 2936ccfcf49..37030a95b89 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -29,6 +29,7 @@ import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; +import { CustomTimeRangeCompare } from '../scene/CustomTimeRangeCompare'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; @@ -407,6 +408,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem { $behaviors: [], extendPanelContext: setDashboardPanelContext, _UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel), + headerActions: config.featureToggles.timeComparison + ? [new CustomTimeRangeCompare({ key: 'time-compare', compareWith: undefined, compareOptions: [] })] + : undefined, }; if (panel.libraryPanel) { diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 7a5c41a08cd..1ab2c72969a 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -17,6 +17,7 @@ import { Dashboard, Panel, RowPanel } from '@grafana/schema'; import { createLogger } from '@grafana/ui'; import { initialIntervalVariableModelState } from 'app/features/variables/interval/reducer'; +import { CustomTimeRangeCompare } from '../scene/CustomTimeRangeCompare'; import { DashboardDatasourceBehaviour } from '../scene/DashboardDatasourceBehaviour'; import { DashboardLayoutOrchestrator } from '../scene/DashboardLayoutOrchestrator'; import { DashboardScene, DashboardSceneState } from '../scene/DashboardScene'; @@ -343,6 +344,9 @@ export function getDefaultVizPanel(): VizPanel { menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior], }), + headerActions: config.featureToggles.timeComparison + ? [new CustomTimeRangeCompare({ key: 'time-compare', compareWith: undefined, compareOptions: [] })] + : undefined, $data: new SceneDataTransformer({ $data: new SceneQueryRunner({ queries: [{ refId: 'A' }], diff --git a/public/app/plugins/panel/timeseries/module.tsx b/public/app/plugins/panel/timeseries/module.tsx index beccdf480d6..0c909935145 100644 --- a/public/app/plugins/panel/timeseries/module.tsx +++ b/public/app/plugins/panel/timeseries/module.tsx @@ -1,5 +1,6 @@ import { PanelPlugin } from '@grafana/data'; import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { commonOptionsBuilder } from '@grafana/ui'; import { optsWithHideZeros } from '@grafana/ui/internal'; @@ -17,6 +18,10 @@ export const plugin = new PanelPlugin(TimeSeriesPanel) commonOptionsBuilder.addTooltipOptions(builder, false, true, optsWithHideZeros); commonOptionsBuilder.addLegendOptions(builder); + if (config.featureToggles.timeComparison) { + commonOptionsBuilder.addTimeCompareOption(builder); + } + builder.addCustomEditor({ id: 'timezone', name: t('timeseries.name-time-zone', 'Time zone'), diff --git a/public/app/plugins/panel/timeseries/panelcfg.cue b/public/app/plugins/panel/timeseries/panelcfg.cue index 0d2f6decd67..71ac34bd897 100644 --- a/public/app/plugins/panel/timeseries/panelcfg.cue +++ b/public/app/plugins/panel/timeseries/panelcfg.cue @@ -25,6 +25,7 @@ composableKinds: PanelCfg: lineage: { Options: common.OptionsWithTimezones & { legend: common.VizLegendOptions tooltip: common.VizTooltipOptions + timeCompare?: common.TimeCompareOptions orientation?: common.VizOrientation } @cuetsy(kind="interface") diff --git a/public/app/plugins/panel/timeseries/panelcfg.gen.ts b/public/app/plugins/panel/timeseries/panelcfg.gen.ts index e5e4bce7e7b..95eb3a710f8 100644 --- a/public/app/plugins/panel/timeseries/panelcfg.gen.ts +++ b/public/app/plugins/panel/timeseries/panelcfg.gen.ts @@ -13,6 +13,7 @@ import * as common from '@grafana/schema'; export interface Options extends common.OptionsWithTimezones { legend: common.VizLegendOptions; orientation?: common.VizOrientation; + timeCompare?: common.TimeCompareOptions; tooltip: common.VizTooltipOptions; } From 46262071559b3b1c8bdb681bd93ad4095c119bb1 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 00:40:42 +0000 Subject: [PATCH 083/101] I18n: Download translations from Crowdin (#108647) 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 | 26 +++++++++++++++----------- public/locales/de-DE/grafana.json | 26 +++++++++++++++----------- public/locales/es-ES/grafana.json | 26 +++++++++++++++----------- public/locales/fr-FR/grafana.json | 26 +++++++++++++++----------- public/locales/hu-HU/grafana.json | 26 +++++++++++++++----------- public/locales/id-ID/grafana.json | 26 +++++++++++++++----------- public/locales/it-IT/grafana.json | 26 +++++++++++++++----------- public/locales/ja-JP/grafana.json | 26 +++++++++++++++----------- public/locales/ko-KR/grafana.json | 26 +++++++++++++++----------- public/locales/nl-NL/grafana.json | 26 +++++++++++++++----------- public/locales/pl-PL/grafana.json | 26 +++++++++++++++----------- public/locales/pt-BR/grafana.json | 26 +++++++++++++++----------- public/locales/pt-PT/grafana.json | 26 +++++++++++++++----------- public/locales/ru-RU/grafana.json | 26 +++++++++++++++----------- public/locales/sv-SE/grafana.json | 26 +++++++++++++++----------- public/locales/tr-TR/grafana.json | 26 +++++++++++++++----------- public/locales/zh-Hans/grafana.json | 26 +++++++++++++++----------- public/locales/zh-Hant/grafana.json | 26 +++++++++++++++----------- 18 files changed, 270 insertions(+), 198 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 0b6722816ae..74c68092a73 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -7204,6 +7204,7 @@ "start-time": "Čas zahájení:" } }, + "share-span": "", "warnings": "Varování" }, "span-filters": { @@ -11017,15 +11018,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11141,10 +11142,10 @@ "title-instant-updates-requests-webhooks": "Okamžité aktualizace a pull requesty pomocí webhooků.", "title-visual-previews-in-pull-requests": "Vizuální náhledy v pull requestech s vykreslením obrázků" }, + "error-title-default": "", "expanded-row": { "job-specification": "Specifikace úlohy", - "summary": "Shrnutí", - "title-error": "Chyba" + "summary": "Shrnutí" }, "features-list": { "actions": { @@ -11289,10 +11290,12 @@ "starting": "Spouštění…", "status": { "title-error-running-job": "Chyba při spouštění úlohy", - "title-job-completed-successfully": "Úloha byla úspěšně dokončena", "title-warning-running-job": "" }, - "summary": "Shrnutí" + "summary": "Shrnutí", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11458,6 +11461,7 @@ "go-to": "Přejít na", "make-sure": "Nezapomeňte zahrnout tato oprávnění" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Během tohoto procesu nebude možné vytvářet, upravovat ani odstraňovat zdroje. V posledním kroku zmizí.", "alert-point-2": "Po zajištění se zdroje znovu zobrazí a budou spravovány prostřednictvím externího úložiště.", diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index b3056eae2d5..0139a868dc4 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Startzeit:" } }, + "share-span": "", "warnings": "Warnungen" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Sofortige Updates und Pull-Requests mit Webhooks.", "title-visual-previews-in-pull-requests": "Visuelle Vorschauen in Pull-Requests mit Bildwiedergabe" }, + "error-title-default": "", "expanded-row": { "job-specification": "Auftragsspezifikation", - "summary": "Zusammenfassung", - "title-error": "Fehler" + "summary": "Zusammenfassung" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Wird gestartet ...", "status": { "title-error-running-job": "Fehler bei der Ausführung des Auftrags", - "title-job-completed-successfully": "Auftrag erfolgreich abgeschlossen", "title-warning-running-job": "" }, - "summary": "Zusammenfassung" + "summary": "Zusammenfassung", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Weiter zu", "make-sure": "Achten Sie darauf, dass diese Berechtigungen berücksichtigt werden" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Während dieses Vorgangs können keine Ressourcen erstellt, bearbeitet oder gelöscht werden. Im letzten Schritt werden sie entfernt.", "alert-point-2": "Sobald die Bereitstellung abgeschlossen ist, werden die Ressourcen wieder angezeigt und über einen externen Speicher verwaltet.", diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 3fd26a8acaf..644fb0b08d5 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Hora de comienzo:" } }, + "share-span": "", "warnings": "Advertencias" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Actualizaciones instantáneas y solicitudes de extracción con webhooks.", "title-visual-previews-in-pull-requests": "Vistas previas visuales en solicitudes de extracción con renderizado de imágenes" }, + "error-title-default": "", "expanded-row": { "job-specification": "Especificación del trabajo", - "summary": "Resumen", - "title-error": "Error" + "summary": "Resumen" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Empezando...", "status": { "title-error-running-job": "Error al ejecutar el trabajo", - "title-job-completed-successfully": "Trabajo completado con éxito", "title-warning-running-job": "" }, - "summary": "Resumen" + "summary": "Resumen", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Ir a", "make-sure": "Asegúrate de incluir estos permisos" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Los recursos no se podrán crear, editar ni eliminar durante este proceso. En el último paso, desaparecerán.", "alert-point-2": "Una vez que se complete el aprovisionamiento, los recursos volverán a aparecer y se gestionarán a través de un almacenamiento externo.", diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 6278e48a1d8..c062ee51b3c 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Heure de début :" } }, + "share-span": "", "warnings": "Avertissements" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Mises à jour instantanées et demandes de fusion avec les webhooks.", "title-visual-previews-in-pull-requests": "Aperçus visuels dans les demandes de fusion avec rendu d’image" }, + "error-title-default": "", "expanded-row": { "job-specification": "Description de la mission", - "summary": "Synthèse", - "title-error": "Erreur" + "summary": "Synthèse" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Démarrage...", "status": { "title-error-running-job": "Erreur lors de l’exécution de la mission", - "title-job-completed-successfully": "Mission accomplie", "title-warning-running-job": "" }, - "summary": "Synthèse" + "summary": "Synthèse", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Accéder à", "make-sure": "Assurez-vous d’inclure ces autorisations" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Les ressources ne pourront pas être créées, modifiées ou supprimées pendant ce processus. Lors de la dernière étape, elles disparaîtront.", "alert-point-2": "Une fois la mise en service terminée, les ressources réapparaîtront et seront gérées via un stockage externe.", diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index 7d45de61805..2310aa7674f 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Kezdés időpontja:" } }, + "share-span": "", "warnings": "Figyelmeztetések" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Azonnali frissítések és lekérések webkapcsokkal.", "title-visual-previews-in-pull-requests": "Vizuális előnézetek a lekérésekben képalkotással" }, + "error-title-default": "", "expanded-row": { "job-specification": "Feladat részletei", - "summary": "Összegzés", - "title-error": "Hiba" + "summary": "Összegzés" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Kezdés…", "status": { "title-error-running-job": "Hiba történt a feladat futtatásakor", - "title-job-completed-successfully": "A feladat sikeresen befejeződött", "title-warning-running-job": "" }, - "summary": "Összegzés" + "summary": "Összegzés", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Tovább ide:", "make-sure": "Mindenképpen foglalja bele ezeket az engedélyeket" }, + "warning-title-default": "", "wizard": { "alert-point-1": "A folyamat során nem lehet erőforrásokat létrehozni, szerkeszteni vagy törölni. Az utolsó lépésben el fognak tűnni.", "alert-point-2": "A kiépítés befejezése után az erőforrások újra megjelennek, és külső tárolóeszközön keresztül kezelhetők.", diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index d1a2a2ae3bd..54e2e6d381c 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -7138,6 +7138,7 @@ "start-time": "Waktu Mulai:" } }, + "share-span": "", "warnings": "Peringatan" }, "span-filters": { @@ -10918,15 +10919,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11033,10 +11034,10 @@ "title-instant-updates-requests-webhooks": "Pembaruan instan dan permintaan pull dengan webhook.", "title-visual-previews-in-pull-requests": "Pratinjau visual dalam permintaan pull dengan rendering gambar" }, + "error-title-default": "", "expanded-row": { "job-specification": "Spesifikasi Pekerjaan", - "summary": "Ringkasan", - "title-error": "Kesalahan" + "summary": "Ringkasan" }, "features-list": { "actions": { @@ -11181,10 +11182,12 @@ "starting": "Memulai...", "status": { "title-error-running-job": "Kesalahan saat menjalankan pekerjaan", - "title-job-completed-successfully": "Pekerjaan berhasil diselesaikan", "title-warning-running-job": "" }, - "summary": "Ringkasan" + "summary": "Ringkasan", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11350,6 +11353,7 @@ "go-to": "Buka", "make-sure": "Pastikan Anda menyertakan izin ini" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Sumber daya tidak akan dapat dibuat, diedit, atau dihapus selama proses ini. Pada langkah terakhir, sumber daya akan menghilang.", "alert-point-2": "Setelah penyediaan selesai, sumber daya akan muncul kembali dan dikelola melalui penyimpanan eksternal.", diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 46784bccbba..b907d962cbb 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "" } }, + "share-span": "", "warnings": "Allarmi" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Aggiornamenti istantanei e richieste di pull con webhook.", "title-visual-previews-in-pull-requests": "Anteprime visive nelle richieste di pull con rendering delle immagini" }, + "error-title-default": "", "expanded-row": { "job-specification": "Specifiche dell'attività", - "summary": "Indice", - "title-error": "Errore" + "summary": "Indice" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Avvio in corso...", "status": { "title-error-running-job": "Errore durante l'esecuzione dell'attività", - "title-job-completed-successfully": "Attività completata con successo", "title-warning-running-job": "" }, - "summary": "Indice" + "summary": "Indice", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Vai a", "make-sure": "Assicurati di includere queste autorizzazioni" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Le risorse non potranno essere create, modificate o eliminate durante questo processo. Nell'ultimo passaggio, scompariranno.", "alert-point-2": "Una volta completato il provisioning, le risorse riappariranno e verranno gestite tramite la memoria esterna.", diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index d5b9ae47f12..29514e30b14 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -7138,6 +7138,7 @@ "start-time": "開始時間:" } }, + "share-span": "", "warnings": "警告" }, "span-filters": { @@ -10918,15 +10919,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11033,10 +11034,10 @@ "title-instant-updates-requests-webhooks": "Webhookによるリアルタイム更新とプルリクエスト。", "title-visual-previews-in-pull-requests": "画像レンダリングによるプルリクエストの視覚的プレビュー" }, + "error-title-default": "", "expanded-row": { "job-specification": "ジョブ仕様", - "summary": "概要", - "title-error": "エラー" + "summary": "概要" }, "features-list": { "actions": { @@ -11181,10 +11182,12 @@ "starting": "開始中...", "status": { "title-error-running-job": "実行エラー", - "title-job-completed-successfully": "ジョブが正常に完了しました", "title-warning-running-job": "" }, - "summary": "概要" + "summary": "概要", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11350,6 +11353,7 @@ "go-to": "進む", "make-sure": "これらの権限を必ず含めてください" }, + "warning-title-default": "", "wizard": { "alert-point-1": "このプロセス中はリソースの作成、編集、削除を行えません。最後のステップになると、リソースが消えます。", "alert-point-2": "プロビジョニングが完了すると、リソースが再表示され、外部ストレージを通じて管理されます。", diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index deeda946df1..5abaa828353 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -7138,6 +7138,7 @@ "start-time": "시작 시간:" } }, + "share-span": "", "warnings": "경고" }, "span-filters": { @@ -10918,15 +10919,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11033,10 +11034,10 @@ "title-instant-updates-requests-webhooks": "웹훅을 통한 즉시 업데이트 및 풀 요청.", "title-visual-previews-in-pull-requests": "이미지 렌더링을 통해 풀 요청을 시각적으로 미리 보기" }, + "error-title-default": "", "expanded-row": { "job-specification": "작업 사양", - "summary": "요약", - "title-error": "오류" + "summary": "요약" }, "features-list": { "actions": { @@ -11181,10 +11182,12 @@ "starting": "시작하는 중...", "status": { "title-error-running-job": "작업 실행 중 오류 발생", - "title-job-completed-successfully": "작업이 성공적으로 완료되었습니다", "title-warning-running-job": "" }, - "summary": "요약" + "summary": "요약", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11350,6 +11353,7 @@ "go-to": "다음으로 이동", "make-sure": "반드시 이러한 권한을 포함하세요" }, + "warning-title-default": "", "wizard": { "alert-point-1": "이 프로세스 중에는 리소스를 생성, 편집 또는 삭제할 수 없습니다. 마지막 단계에서는 리소스가 표시되지 않습니다.", "alert-point-2": "프로비저닝이 완료되면 리소스가 다시 표시되고 외부 스토리지를 통해 관리됩니다.", diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 93b2b0337d5..11af820962a 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Starttijd:" } }, + "share-span": "", "warnings": "Waarschuwingen" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Directe updates en pull requests met webhooks.", "title-visual-previews-in-pull-requests": "Visuele voorbeelden in pull requests met beeldweergave" }, + "error-title-default": "", "expanded-row": { "job-specification": "Taakbeschrijving", - "summary": "Samenvatting", - "title-error": "Fout" + "summary": "Samenvatting" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Starten...", "status": { "title-error-running-job": "Er is een fout opgetreden bij het uitvoeren van de taak", - "title-job-completed-successfully": "Taak is voltooid", "title-warning-running-job": "" }, - "summary": "Samenvatting" + "summary": "Samenvatting", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Ga naar", "make-sure": "Zorg ervoor dat je deze toestemmingen opneemt" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Bronnen kunnen tijdens dit proces niet worden gemaakt, bewerkt of verwijderd. In de laatste stap verdwijnen ze.", "alert-point-2": "Zodra de provisioning is voltooid, verschijnen bronnen weer en worden ze beheerd via externe opslag.", diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index e41d5c348f8..a12c4bff317 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -7204,6 +7204,7 @@ "start-time": "Czas rozpoczęcia:" } }, + "share-span": "", "warnings": "Ostrzeżenia" }, "span-filters": { @@ -11017,15 +11018,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11141,10 +11142,10 @@ "title-instant-updates-requests-webhooks": "Błyskawiczne aktualizacje i żądania pull dzięki webhookom.", "title-visual-previews-in-pull-requests": "Podglądy wizualne w żądaniach pull z renderowaniem obrazów" }, + "error-title-default": "", "expanded-row": { "job-specification": "Specyfikacja zadania", - "summary": "Podsumowanie", - "title-error": "Błąd" + "summary": "Podsumowanie" }, "features-list": { "actions": { @@ -11289,10 +11290,12 @@ "starting": "Uruchamianie…", "status": { "title-error-running-job": "Błąd uruchamiania zadania", - "title-job-completed-successfully": "Zadanie zakończone pomyślnie", "title-warning-running-job": "" }, - "summary": "Podsumowanie" + "summary": "Podsumowanie", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11458,6 +11461,7 @@ "go-to": "Przejdź do", "make-sure": "Pamiętaj, aby uwzględnić te uprawnienia" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Podczas tego procesu nie będzie można tworzyć, edytować ani usuwać zasobów. W ostatnim kroku zasoby znikną.", "alert-point-2": "Po zakończeniu aprowizacji zasoby pojawią się ponownie i będą zarządzane za pośrednictwem zewnętrznej pamięci masowej.", diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index 78144d213ba..055ea9c3f22 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Horário de início:" } }, + "share-span": "", "warnings": "Avisos" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Atualizações imediatas e solicitações de extração com webhooks.", "title-visual-previews-in-pull-requests": "Pré-visualizações em solicitações de extração com renderização de imagens" }, + "error-title-default": "", "expanded-row": { "job-specification": "Especificação da tarefa", - "summary": "Resumo", - "title-error": "Erro" + "summary": "Resumo" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Iniciando…", "status": { "title-error-running-job": "Erro ao executar a tarefa", - "title-job-completed-successfully": "Tarefa concluída com sucesso", "title-warning-running-job": "" }, - "summary": "Resumo" + "summary": "Resumo", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Ir para", "make-sure": "Certifique-se de incluir essas permissões" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Os recursos não poderão ser criados, editados ou excluídos durante este processo. Na última etapa, eles desaparecerão.", "alert-point-2": "Assim que o provisionamento for concluído, os recursos reaparecerão e serão gerenciados por meio de armazenamento externo.", diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index 4145a491e1f..a2299cdc97a 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Hora de início:" } }, + "share-span": "", "warnings": "Avisos" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Atualizações instantâneas e pedidos de extração com webhooks.", "title-visual-previews-in-pull-requests": "Pré-visualizações em pedidos de extração com renderização de imagens" }, + "error-title-default": "", "expanded-row": { "job-specification": "Definição do trabalho", - "summary": "Resumo", - "title-error": "Erro" + "summary": "Resumo" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "A iniciar...", "status": { "title-error-running-job": "Erro ao executar tarefa", - "title-job-completed-successfully": "Trabalho concluído com sucesso", "title-warning-running-job": "" }, - "summary": "Resumo" + "summary": "Resumo", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Aceda a", "make-sure": "Certifique-se de incluir estas permissões" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Os recursos não poderão ser criados, editados ou eliminados durante este processo. No último passo, irão desaparecer.", "alert-point-2": "Assim que o provisionamento estiver concluído, os recursos reaparecerão e serão geridos através de um armazenamento externo.", diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index d2f6a44a18d..caf5f5513fa 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -7204,6 +7204,7 @@ "start-time": "Время начала:" } }, + "share-span": "", "warnings": "Предупреждения" }, "span-filters": { @@ -11017,15 +11018,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11141,10 +11142,10 @@ "title-instant-updates-requests-webhooks": "Мгновенные обновления и запросы на включение изменений с помощью веб-перехватчиков", "title-visual-previews-in-pull-requests": "Просмотр в запросах на включение изменений с визуализацией изображений" }, + "error-title-default": "", "expanded-row": { "job-specification": "Спецификация задания", - "summary": "Сводка", - "title-error": "Ошибка" + "summary": "Сводка" }, "features-list": { "actions": { @@ -11289,10 +11290,12 @@ "starting": "Запуск...", "status": { "title-error-running-job": "Ошибка при выполнении задания", - "title-job-completed-successfully": "Задание завершено", "title-warning-running-job": "" }, - "summary": "Сводка" + "summary": "Сводка", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11458,6 +11461,7 @@ "go-to": "Перейти к", "make-sure": "Обязательно включите эти разрешения" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Во время этого процесса невозможно создавать, редактировать или удалять ресурсы. На последнем шаге они исчезнут.", "alert-point-2": "После завершения подготовки ресурсы снова появятся и будут управляться через внешнее хранилище.", diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index adbc94f9c1c..c347596c3fb 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Starttid:" } }, + "share-span": "", "warnings": "Varningar" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Omedelbara uppdateringar och pull-förfrågningar med webhooks.", "title-visual-previews-in-pull-requests": "Visuella förhandsgranskningar i pull-förfrågningar med bildrendering" }, + "error-title-default": "", "expanded-row": { "job-specification": "Jobbeskrivning", - "summary": "Sammanfattning", - "title-error": "Fel" + "summary": "Sammanfattning" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Startar …", "status": { "title-error-running-job": "Ett fel uppstod när ett jobb kördes", - "title-job-completed-successfully": "Jobbet har slutförts", "title-warning-running-job": "" }, - "summary": "Sammanfattning" + "summary": "Sammanfattning", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Gå till", "make-sure": "Se till att inkludera dessa behörigheter" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Resurser kan inte skapas, redigeras eller raderas under den här processen. I det sista steget kommer de att försvinna.", "alert-point-2": "När provisionering är klar kommer resurserna att visas igen och kan hanteras via extern lagring.", diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index aa90d1b2954..b4c6e0e31e7 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -7160,6 +7160,7 @@ "start-time": "Başlangıç Tarihi:" } }, + "share-span": "", "warnings": "Uyarılar" }, "span-filters": { @@ -10951,15 +10952,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11069,10 +11070,10 @@ "title-instant-updates-requests-webhooks": "Web kancalarıyla anlık güncellemeler ve çekme istekleri.", "title-visual-previews-in-pull-requests": "Çekme isteklerinde resimlerin işlenmesiyle birlikte görsel ön izleme" }, + "error-title-default": "", "expanded-row": { "job-specification": "İşin Özellikleri", - "summary": "Özet", - "title-error": "Hata" + "summary": "Özet" }, "features-list": { "actions": { @@ -11217,10 +11218,12 @@ "starting": "Başlatılıyor...", "status": { "title-error-running-job": "İş çalıştırılırken hata oluştu", - "title-job-completed-successfully": "İş başarıyla tamamlandı", "title-warning-running-job": "" }, - "summary": "Özet" + "summary": "Özet", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11386,6 +11389,7 @@ "go-to": "Şuraya git:", "make-sure": "Bu izinleri eklediğinizden emin olun" }, + "warning-title-default": "", "wizard": { "alert-point-1": "Bu işlem sırasında kaynaklar oluşturulamayacak, düzenlenemeyecek veya silinemeyecek. Son adımda kaynaklar kaybolacaktır.", "alert-point-2": "Sağlama işlemi tamamlandığında kaynaklar tekrar görünecek ve harici depolama üzerinden yönetilecektir.", diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 003beb4e68f..7ebd76f8f4b 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -7138,6 +7138,7 @@ "start-time": "开始时间:" } }, + "share-span": "", "warnings": "警告" }, "span-filters": { @@ -10918,15 +10919,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11033,10 +11034,10 @@ "title-instant-updates-requests-webhooks": "通过 Webhook 实现即时更新和拉取请求。", "title-visual-previews-in-pull-requests": "具有图像渲染功能的拉取请求可视化预览" }, + "error-title-default": "", "expanded-row": { "job-specification": "作业规范", - "summary": "摘要", - "title-error": "错误" + "summary": "摘要" }, "features-list": { "actions": { @@ -11181,10 +11182,12 @@ "starting": "正在启动...", "status": { "title-error-running-job": "运行作业时出错", - "title-job-completed-successfully": "作业已成功完成", "title-warning-running-job": "" }, - "summary": "摘要" + "summary": "摘要", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11350,6 +11353,7 @@ "go-to": "前往", "make-sure": "务必包含以下权限" }, + "warning-title-default": "", "wizard": { "alert-point-1": "在此过程中将无法创建、编辑或删除资源。在最后一步中,它们将消失。", "alert-point-2": "预配完成后,资源将重新出现并通过外部存储进行管理。", diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 375858bf88d..a107cb4664d 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -7138,6 +7138,7 @@ "start-time": "開始時間:" } }, + "share-span": "", "warnings": "警告" }, "span-filters": { @@ -10918,15 +10919,15 @@ }, "provisioned-resource-preview-banner": { "preview-banner": { - "behind-branch": "", - "new-branch": "", + "behind-branch-text": "", "not-saved": "", - "open-git-hub": "", - "open-pull-request-in-git-hub": "", - "view-pull-request-in-git-hub": "" + "open-in-repo-button": "", + "open-pull-request-in-repo": "", + "view-in-repo-button": "", + "view-pull-request-in-repo": "" }, - "title-created-branch-git-hub": "", - "title-loaded-pull-request-git-hub": "" + "title-created-branch-in-repo": "", + "title-loaded-pull-request-in-repo": "" }, "provisioning": { "banner": { @@ -11033,10 +11034,10 @@ "title-instant-updates-requests-webhooks": "透過 webhook 即時更新和拉取請求。", "title-visual-previews-in-pull-requests": "具有圖片轉譯的拉取請求中的視覺預覽" }, + "error-title-default": "", "expanded-row": { "job-specification": "作業規範", - "summary": "摘要", - "title-error": "錯誤" + "summary": "摘要" }, "features-list": { "actions": { @@ -11181,10 +11182,12 @@ "starting": "正在開始…", "status": { "title-error-running-job": "執行作業時發生錯誤", - "title-job-completed-successfully": "作業已成功完成", "title-warning-running-job": "" }, - "summary": "摘要" + "summary": "摘要", + "title": { + "error-fetching-active-job": "" + } }, "local": { "path-description": "", @@ -11350,6 +11353,7 @@ "go-to": "前往", "make-sure": "請務必包含以下權限" }, + "warning-title-default": "", "wizard": { "alert-point-1": "在此過程中,將無法建立、編輯或刪除資源。在最後一步中,它們將會消失。", "alert-point-2": "佈建完成後,資源將重新出現,並透過外部儲存空間進行管理。", From 22bcd4dab4523ee74571442ebe98ecdbcb7dcdc5 Mon Sep 17 00:00:00 2001 From: Kevin Minehart <5140827+kminehart@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:25:26 -0500 Subject: [PATCH 084/101] Chore: bump version to 12.2.0-pre (#108623) * bump version to 12.2.0-pre * make gen-cue --- .../grafana-extensionstest-app/package.json | 2 +- .../grafana-test-datasource/package.json | 2 +- lerna.json | 2 +- package.json | 2 +- packages/grafana-alerting/package.json | 2 +- packages/grafana-data/package.json | 6 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-eslint-rules/package.json | 2 +- packages/grafana-flamegraph/package.json | 6 +- packages/grafana-i18n/package.json | 2 +- .../grafana-o11y-ds-frontend/package.json | 12 +- packages/grafana-plugin-configs/package.json | 2 +- packages/grafana-prometheus/package.json | 14 +- packages/grafana-runtime/package.json | 10 +- packages/grafana-schema/package.json | 2 +- .../x/AnnotationsListPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/BarChartPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/BarGaugePanelCfg_types.gen.ts | 2 +- .../x/CandlestickPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/CanvasPanelCfg_types.gen.ts | 2 +- .../x/CloudWatchDataQuery_types.gen.ts | 2 +- .../x/DashboardListPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/DatagridPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/DebugPanelCfg_types.gen.ts | 2 +- .../x/ElasticsearchDataQuery_types.gen.ts | 2 +- .../panelcfg/x/GaugePanelCfg_types.gen.ts | 2 +- .../panelcfg/x/GeomapPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/HeatmapPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/HistogramPanelCfg_types.gen.ts | 2 +- .../logs/panelcfg/x/LogsPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/LogsNewPanelCfg_types.gen.ts | 2 +- .../news/panelcfg/x/NewsPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/NodeGraphPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/PieChartPanelCfg_types.gen.ts | 2 +- .../stat/panelcfg/x/StatPanelCfg_types.gen.ts | 2 +- .../x/StateTimelinePanelCfg_types.gen.ts | 2 +- .../x/StatusHistoryPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/TablePanelCfg_types.gen.ts | 2 +- .../text/panelcfg/x/TextPanelCfg_types.gen.ts | 2 +- .../x/TimeSeriesPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/TrendPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/XYChartPanelCfg_types.gen.ts | 2 +- packages/grafana-sql/package.json | 12 +- packages/grafana-test-utils/package.json | 2 +- packages/grafana-ui/package.json | 10 +- .../datasource/azuremonitor/package.json | 16 +- .../datasource/cloud-monitoring/package.json | 14 +- .../package.json | 14 +- .../grafana-pyroscope-datasource/package.json | 12 +- .../grafana-testdata-datasource/package.json | 14 +- .../plugins/datasource/jaeger/package.json | 2 +- .../app/plugins/datasource/loki/package.json | 14 +- .../app/plugins/datasource/mssql/package.json | 16 +- .../app/plugins/datasource/mysql/package.json | 14 +- .../app/plugins/datasource/parca/package.json | 12 +- .../app/plugins/datasource/tempo/package.json | 4 +- .../plugins/datasource/zipkin/package.json | 2 +- yarn.lock | 182 +++++++++--------- 58 files changed, 231 insertions(+), 231 deletions(-) diff --git a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json index d11250bdc4d..89d6c90be5d 100644 --- a/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e-playwright/test-plugins/grafana-extensionstest-app/package.json @@ -1,6 +1,6 @@ { "name": "@test-plugins/extensions-test-app", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "private": true, "scripts": { "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", diff --git a/e2e-playwright/test-plugins/grafana-test-datasource/package.json b/e2e-playwright/test-plugins/grafana-test-datasource/package.json index 9f976341268..7ed88373c45 100644 --- a/e2e-playwright/test-plugins/grafana-test-datasource/package.json +++ b/e2e-playwright/test-plugins/grafana-test-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@test-plugins/grafana-e2etest-datasource", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "private": true, "scripts": { "build": "NODE_OPTIONS='--experimental-strip-types --no-warnings=ExperimentalWarning' webpack -c ./webpack.config.ts --env production", diff --git a/lerna.json b/lerna.json index 71b3ceeef8f..fe2cdbf0402 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", "npmClient": "yarn", - "version": "12.1.0-pre" + "version": "12.2.0-pre" } diff --git a/package.json b/package.json index b2518a6804e..c76a660b530 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "repository": "github:grafana/grafana", "scripts": { "predev": "./scripts/check-frontend-dev.sh", diff --git a/packages/grafana-alerting/package.json b/packages/grafana-alerting/package.json index b505072280c..0d4dfa1c746 100644 --- a/packages/grafana-alerting/package.json +++ b/packages/grafana-alerting/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/alerting", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana Alerting Library – Build vertical integrations on top of the industry-leading alerting solution", "keywords": [ "typescript", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 6012ea32a6e..8029328d12e 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -56,8 +56,8 @@ }, "dependencies": { "@braintree/sanitize-url": "7.0.1", - "@grafana/i18n": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", + "@grafana/i18n": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", "@leeoniya/ufuzzy": "1.0.18", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.3", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 98ac486bafb..d647d0fb88d 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 35819b4ef6b..f83f1f679d9 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -1,7 +1,7 @@ { "name": "@grafana/eslint-plugin", "description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "main": "./index.cjs", "author": "Grafana Labs", "license": "Apache-2.0", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 41951af9fbc..f8597f17c32 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/flamegraph", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana flamegraph visualization component", "keywords": [ "grafana", @@ -44,8 +44,8 @@ ], "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "@leeoniya/ufuzzy": "1.0.18", "d3": "^7.8.5", "lodash": "4.17.21", diff --git a/packages/grafana-i18n/package.json b/packages/grafana-i18n/package.json index 9f2fd298de1..6ffd95918f4 100644 --- a/packages/grafana-i18n/package.json +++ b/packages/grafana-i18n/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/i18n", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana Internationalization Library", "keywords": [ "grafana", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 9627f5d6b10..1d56eb47a0e 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "name": "@grafana/o11y-ds-frontend", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Library to manage traces in Grafana.", "sideEffects": false, "repository": { @@ -18,12 +18,12 @@ }, "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", - "@grafana/e2e-selectors": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", "@grafana/plugin-ui": "0.10.7", - "@grafana/runtime": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "react-select": "5.10.2", "react-use": "17.6.0", "rxjs": "7.8.2", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index 5213f1f91f9..b42fc2ce215 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -2,7 +2,7 @@ "name": "@grafana/plugin-configs", "description": "Shared dependencies and files for core plugins", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "tslib": "2.8.1" }, diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 6b55fde3883..765c9e5434b 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "AGPL-3.0-only", "name": "@grafana/prometheus", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana Prometheus Library", "keywords": [ "typescript", @@ -41,13 +41,13 @@ "dependencies": { "@emotion/css": "11.13.5", "@floating-ui/react": "0.27.13", - "@grafana/data": "12.1.0-pre", - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/i18n": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/i18n": "12.2.0-pre", "@grafana/plugin-ui": "0.10.7", - "@grafana/runtime": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "@hello-pangea/dnd": "18.0.1", "@leeoniya/ufuzzy": "1.0.18", "@lezer/common": "1.2.3", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index ed4af1a250b..f32cc4639da 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -53,11 +53,11 @@ "postpack": "mv package.json.bak package.json && rimraf ./unstable" }, "dependencies": { - "@grafana/data": "12.1.0-pre", - "@grafana/e2e-selectors": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", "@grafana/faro-web-sdk": "^1.13.2", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "@types/systemjs": "6.15.3", "history": "4.10.1", "lodash": "4.17.21", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index a42468a1ef8..9896376d716 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts index 6d79fbeceb8..63bb4339949 100644 --- a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options { limit: number; diff --git a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts index 1bc5ff0f497..59924a010ae 100644 --- a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithTextFormatting { /** diff --git a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts index 4345b7fae05..c086da202b8 100644 --- a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options extends common.OptionsWithLegend, common.SingleStatBaseOptions { displayMode: common.BarGaugeDisplayMode; diff --git a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts index ee96c69c0a9..ea817787d65 100644 --- a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export enum VizDisplayMode { Candles = 'candles', diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 84c0c84408d..983129db72b 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export enum HorizontalConstraint { Center = 'center', diff --git a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts index 9f339bc8cc4..f0e4394451d 100644 --- a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface MetricStat { /** diff --git a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts index 328429a5829..1348c30dbad 100644 --- a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts index 99b9e516dba..d8db9ef7edf 100644 --- a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options { selectedSeries: number; diff --git a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts index 6967fd64ccb..811836dff43 100644 --- a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export type UpdateConfig = { render: boolean, diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index 7cac1eab340..dfb50c2c981 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested); diff --git a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts index abe40c63645..36aa14da169 100644 --- a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options extends common.SingleStatBaseOptions { minVizHeight: number; diff --git a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts index 14a718cf6d9..0a085d8ec6e 100644 --- a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options { basemap: ui.MapLayerOptions; diff --git a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts index b87a6c3897f..b0a2a2b5233 100644 --- a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; /** * Controls the color mode of the heatmap diff --git a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts index 819bb518581..59b82823d78 100644 --- a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip { /** diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index ec66616abc6..da04269f0b3 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options { controlsStorageKey?: string; diff --git a/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts index 13e0ad8c692..2b64bba333e 100644 --- a/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logsnew/panelcfg/x/LogsNewPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options { controlsStorageKey?: string; diff --git a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts index c86b8b987f7..eb0b74331d4 100644 --- a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts index 10f74d337b8..a5e4657464a 100644 --- a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface ArcOption { /** diff --git a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts index 5061e824e18..c5f420a0b11 100644 --- a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; /** * Select the pie chart display style. diff --git a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts index 3f290835b85..8296f089a14 100644 --- a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options extends common.SingleStatBaseOptions { colorMode: common.BigValueColorMode; diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index 40af7a986a7..58b487eb4d3 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index d0cc207400b..0ddf8a30c7a 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts index 2529b11f635..fe36790fe55 100644 --- a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts index b97546048d7..4fbda90032b 100644 --- a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts @@ -8,7 +8,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export enum TextMode { Code = 'code', diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index 8b9d3d48053..30349a9ff9c 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export interface Options extends common.OptionsWithTimezones { legend: common.VizLegendOptions; diff --git a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts index 9d3946018df..aa3b48c20ab 100644 --- a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; /** * Identical to timeseries... except it does not have timezone settings diff --git a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts index 35b7fdfd4e5..13516a37619 100644 --- a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts @@ -10,7 +10,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "12.1.0-pre"; +export const pluginVersion = "12.2.0-pre"; export enum PointShape { Circle = 'circle', diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 607eb010729..574d940a658 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "@grafana/sql", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git", @@ -16,12 +16,12 @@ }, "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/i18n": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/i18n": "12.2.0-pre", "@grafana/plugin-ui": "0.10.7", - "@grafana/runtime": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "@react-awesome-query-builder/ui": "6.6.15", "immutable": "5.1.3", "lodash": "4.17.21", diff --git a/packages/grafana-test-utils/package.json b/packages/grafana-test-utils/package.json index 8ecc4ae8735..7f58c62ac0d 100644 --- a/packages/grafana-test-utils/package.json +++ b/packages/grafana-test-utils/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/test-utils", - "version": "12.0.0-pre", + "version": "12.2.0-pre", "private": true, "description": "Grafana test utils & Mock API", "keywords": [ diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index cdafde28217..6196a2a38a0 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "12.1.0-pre", + "version": "12.2.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -67,11 +67,11 @@ "@emotion/react": "11.14.0", "@emotion/serialize": "1.3.3", "@floating-ui/react": "0.27.13", - "@grafana/data": "12.1.0-pre", - "@grafana/e2e-selectors": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", "@grafana/faro-web-sdk": "^1.13.2", - "@grafana/i18n": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", + "@grafana/i18n": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", "@hello-pangea/dnd": "18.0.1", "@monaco-editor/react": "4.7.0", "@popperjs/core": "2.11.8", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 710827ee4fc..e0828ae1816 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -2,15 +2,15 @@ "name": "@grafana-plugins/grafana-azure-monitor-datasource", "description": "Grafana data source for Azure Monitor", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", - "@grafana/i18n": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/i18n": "12.2.0-pre", "@grafana/plugin-ui": "0.10.7", - "@grafana/runtime": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "@kusto/monaco-kusto": "^10.0.0", "fast-deep-equal": "^3.1.3", "i18next": "^25.0.0", @@ -26,8 +26,8 @@ "tslib": "2.8.1" }, "devDependencies": { - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.3.0", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 5a3825feef2..c27670248a3 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -2,15 +2,15 @@ "name": "@grafana-plugins/stackdriver", "description": "Grafana data source for Google Cloud Monitoring", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", "@grafana/google-sdk": "0.3.4", "@grafana/plugin-ui": "0.10.7", - "@grafana/runtime": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "debounce-promise": "3.1.2", "fast-deep-equal": "^3.1.3", "i18next": "^25.0.0", @@ -26,8 +26,8 @@ "tslib": "2.8.1" }, "devDependencies": { - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.3.0", diff --git a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json index eae4c770430..07af56ca8d6 100644 --- a/public/app/plugins/datasource/grafana-postgresql-datasource/package.json +++ b/public/app/plugins/datasource/grafana-postgresql-datasource/package.json @@ -2,22 +2,22 @@ "name": "@grafana-plugins/grafana-postgresql-datasource", "description": "PostgreSQL data source plugin", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", "@grafana/plugin-ui": "0.10.7", - "@grafana/runtime": "12.1.0-pre", - "@grafana/sql": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/sql": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "lodash": "4.17.21", "react": "18.3.1", "rxjs": "7.8.2", "tslib": "2.8.1" }, "devDependencies": { - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index b1efcc110bc..595dbef2981 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -2,13 +2,13 @@ "name": "@grafana-plugins/grafana-pyroscope-datasource", "description": "Continuous profiling for analysis of CPU and memory usage, down to the line number and throughout time. Saving infrastructure cost, improving performance, and increasing reliability.", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", - "@grafana/runtime": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "fast-deep-equal": "^3.1.3", "lodash": "4.17.21", "monaco-editor": "0.34.1", @@ -20,7 +20,7 @@ "tslib": "2.8.1" }, "devDependencies": { - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.3.0", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index e3fe3f5e838..5118426ef3b 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -2,13 +2,13 @@ "name": "@grafana-plugins/grafana-testdata-datasource", "description": "Generates test data in different forms", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", - "@grafana/runtime": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "d3-random": "^3.0.1", "lodash": "4.17.21", "micro-memoize": "^4.1.2", @@ -21,8 +21,8 @@ "uuid": "11.1.0" }, "devDependencies": { - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.3.0", diff --git a/public/app/plugins/datasource/jaeger/package.json b/public/app/plugins/datasource/jaeger/package.json index 79a2a41f259..34998789a2f 100644 --- a/public/app/plugins/datasource/jaeger/package.json +++ b/public/app/plugins/datasource/jaeger/package.json @@ -2,7 +2,7 @@ "name": "@grafana-plugins/jaeger", "description": "Jaeger plugin for Grafana", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "workspace:*", diff --git a/public/app/plugins/datasource/loki/package.json b/public/app/plugins/datasource/loki/package.json index 02f5fbd586b..096eedc45f7 100644 --- a/public/app/plugins/datasource/loki/package.json +++ b/public/app/plugins/datasource/loki/package.json @@ -2,16 +2,16 @@ "name": "@grafana-plugins/loki", "description": "Loki data source plugin for Grafana", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", "@grafana/lezer-logql": "0.2.8", "@grafana/llm": "0.22.1", "@grafana/monaco-logql": "^0.0.8", - "@grafana/runtime": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "d3-random": "^3.0.1", "lodash": "4.17.21", "micro-memoize": "^4.1.2", @@ -24,8 +24,8 @@ "uuid": "11.1.0" }, "devDependencies": { - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.3.0", diff --git a/public/app/plugins/datasource/mssql/package.json b/public/app/plugins/datasource/mssql/package.json index 704beb2a9d9..f6701f152b0 100644 --- a/public/app/plugins/datasource/mssql/package.json +++ b/public/app/plugins/datasource/mssql/package.json @@ -2,23 +2,23 @@ "name": "@grafana-plugins/mssql", "description": "MSSQL data source plugin", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", - "@grafana/i18n": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/i18n": "12.2.0-pre", "@grafana/plugin-ui": "0.10.7", - "@grafana/runtime": "12.1.0-pre", - "@grafana/sql": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/sql": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "lodash": "4.17.21", "react": "18.3.1", "rxjs": "7.8.2", "tslib": "2.8.1" }, "devDependencies": { - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", diff --git a/public/app/plugins/datasource/mysql/package.json b/public/app/plugins/datasource/mysql/package.json index bc4415966de..a54da9e6b23 100644 --- a/public/app/plugins/datasource/mysql/package.json +++ b/public/app/plugins/datasource/mysql/package.json @@ -2,22 +2,22 @@ "name": "@grafana-plugins/mysql", "description": "MySQL data source plugin", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", "@grafana/plugin-ui": "0.10.7", - "@grafana/runtime": "12.1.0-pre", - "@grafana/sql": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/sql": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "lodash": "4.17.21", "react": "18.3.1", "rxjs": "7.8.2", "tslib": "2.8.1" }, "devDependencies": { - "@grafana/e2e-selectors": "12.1.0-pre", - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/e2e-selectors": "12.2.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index 0f1604b85a7..d411208a322 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -2,13 +2,13 @@ "name": "@grafana-plugins/parca", "description": "Continuous profiling for analysis of CPU and memory usage, down to the line number and throughout time. Saving infrastructure cost, improving performance, and increasing reliability.", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", - "@grafana/data": "12.1.0-pre", - "@grafana/runtime": "12.1.0-pre", - "@grafana/schema": "12.1.0-pre", - "@grafana/ui": "12.1.0-pre", + "@grafana/data": "12.2.0-pre", + "@grafana/runtime": "12.2.0-pre", + "@grafana/schema": "12.2.0-pre", + "@grafana/ui": "12.2.0-pre", "lodash": "4.17.21", "monaco-editor": "0.34.1", "react": "18.3.1", @@ -18,7 +18,7 @@ "tslib": "2.8.1" }, "devDependencies": { - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/react": "16.3.0", "@testing-library/user-event": "14.6.1", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 7828fb86d49..115ba26cad4 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -2,7 +2,7 @@ "name": "@grafana-plugins/tempo", "description": "Grafana plugin for the Tempo data source.", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "workspace:*", @@ -38,7 +38,7 @@ "uuid": "11.1.0" }, "devDependencies": { - "@grafana/plugin-configs": "12.1.0-pre", + "@grafana/plugin-configs": "12.2.0-pre", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.6.3", "@testing-library/react": "16.3.0", diff --git a/public/app/plugins/datasource/zipkin/package.json b/public/app/plugins/datasource/zipkin/package.json index 147cc5a8f00..126bcc8d923 100644 --- a/public/app/plugins/datasource/zipkin/package.json +++ b/public/app/plugins/datasource/zipkin/package.json @@ -2,7 +2,7 @@ "name": "@grafana-plugins/zipkin", "description": "Zipkin plugin for Grafana", "private": true, - "version": "12.1.0-pre", + "version": "12.2.0-pre", "dependencies": { "@emotion/css": "11.13.5", "@grafana/data": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 6444733ff34..c4f5f65234a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2556,14 +2556,14 @@ __metadata: resolution: "@grafana-plugins/grafana-azure-monitor-datasource@workspace:public/app/plugins/datasource/azuremonitor" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/i18n": "npm:12.1.0-pre" - "@grafana/plugin-configs": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" + "@grafana/i18n": "npm:12.2.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@kusto/monaco-kusto": "npm:^10.0.0" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" @@ -2603,13 +2603,13 @@ __metadata: resolution: "@grafana-plugins/grafana-postgresql-datasource@workspace:public/app/plugins/datasource/grafana-postgresql-datasource" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/plugin-configs": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/sql": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/sql": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" @@ -2635,11 +2635,11 @@ __metadata: resolution: "@grafana-plugins/grafana-pyroscope-datasource@workspace:public/app/plugins/datasource/grafana-pyroscope-datasource" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/plugin-configs": "npm:12.1.0-pre" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.3.0" @@ -2675,12 +2675,12 @@ __metadata: resolution: "@grafana-plugins/grafana-testdata-datasource@workspace:public/app/plugins/datasource/grafana-testdata-datasource" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/plugin-configs": "npm:12.1.0-pre" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.3.0" @@ -2759,15 +2759,15 @@ __metadata: resolution: "@grafana-plugins/loki@workspace:public/app/plugins/datasource/loki" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" "@grafana/lezer-logql": "npm:0.2.8" "@grafana/llm": "npm:0.22.1" "@grafana/monaco-logql": "npm:^0.0.8" - "@grafana/plugin-configs": "npm:12.1.0-pre" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.3.0" @@ -2802,14 +2802,14 @@ __metadata: resolution: "@grafana-plugins/mssql@workspace:public/app/plugins/datasource/mssql" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/i18n": "npm:12.1.0-pre" - "@grafana/plugin-configs": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" + "@grafana/i18n": "npm:12.2.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/sql": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/sql": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" @@ -2835,13 +2835,13 @@ __metadata: resolution: "@grafana-plugins/mysql@workspace:public/app/plugins/datasource/mysql" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/plugin-configs": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/sql": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/sql": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" @@ -2867,11 +2867,11 @@ __metadata: resolution: "@grafana-plugins/parca@workspace:public/app/plugins/datasource/parca" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/plugin-configs": "npm:12.1.0-pre" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/react": "npm:16.3.0" "@testing-library/user-event": "npm:14.6.1" @@ -2900,14 +2900,14 @@ __metadata: resolution: "@grafana-plugins/stackdriver@workspace:public/app/plugins/datasource/cloud-monitoring" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" "@grafana/google-sdk": "npm:0.3.4" - "@grafana/plugin-configs": "npm:12.1.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.6.3" "@testing-library/react": "npm:16.3.0" @@ -2952,7 +2952,7 @@ __metadata: "@grafana/lezer-traceql": "npm:0.0.23" "@grafana/monaco-logql": "npm:^0.0.8" "@grafana/o11y-ds-frontend": "workspace:*" - "@grafana/plugin-configs": "npm:12.1.0-pre" + "@grafana/plugin-configs": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" "@grafana/runtime": "workspace:*" "@grafana/schema": "workspace:*" @@ -3117,13 +3117,13 @@ __metadata: languageName: node linkType: hard -"@grafana/data@npm:12.1.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": +"@grafana/data@npm:12.2.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": version: 0.0.0-use.local resolution: "@grafana/data@workspace:packages/grafana-data" dependencies: "@braintree/sanitize-url": "npm:7.0.1" - "@grafana/i18n": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" + "@grafana/i18n": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@leeoniya/ufuzzy": "npm:1.0.18" "@rollup/plugin-node-resolve": "npm:16.0.1" @@ -3171,7 +3171,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@npm:12.1.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": +"@grafana/e2e-selectors@npm:12.2.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": version: 0.0.0-use.local resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors" dependencies: @@ -3272,9 +3272,9 @@ __metadata: "@babel/preset-env": "npm:7.28.0" "@babel/preset-react": "npm:7.27.1" "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@leeoniya/ufuzzy": "npm:1.0.18" "@rollup/plugin-node-resolve": "npm:16.0.1" "@testing-library/dom": "npm:10.4.0" @@ -3323,7 +3323,7 @@ __metadata: languageName: node linkType: hard -"@grafana/i18n@npm:12.1.0-pre, @grafana/i18n@workspace:*, @grafana/i18n@workspace:packages/grafana-i18n": +"@grafana/i18n@npm:12.2.0-pre, @grafana/i18n@workspace:*, @grafana/i18n@workspace:packages/grafana-i18n": version: 0.0.0-use.local resolution: "@grafana/i18n@workspace:packages/grafana-i18n" dependencies: @@ -3394,13 +3394,13 @@ __metadata: resolution: "@grafana/o11y-ds-frontend@workspace:packages/grafana-o11y-ds-frontend" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" "@testing-library/react": "npm:16.3.0" @@ -3424,7 +3424,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/plugin-configs@npm:12.1.0-pre, @grafana/plugin-configs@workspace:*, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs": +"@grafana/plugin-configs@npm:12.2.0-pre, @grafana/plugin-configs@workspace:*, @grafana/plugin-configs@workspace:packages/grafana-plugin-configs": version: 0.0.0-use.local resolution: "@grafana/plugin-configs@workspace:packages/grafana-plugin-configs" dependencies: @@ -3501,14 +3501,14 @@ __metadata: dependencies: "@emotion/css": "npm:11.13.5" "@floating-ui/react": "npm:0.27.13" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/i18n": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" + "@grafana/i18n": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" - "@grafana/runtime": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@hello-pangea/dnd": "npm:18.0.1" "@leeoniya/ufuzzy": "npm:1.0.18" "@lezer/common": "npm:1.2.3" @@ -3567,16 +3567,16 @@ __metadata: languageName: unknown linkType: soft -"@grafana/runtime@npm:12.1.0-pre, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime": +"@grafana/runtime@npm:12.2.0-pre, @grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime": version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" "@grafana/faro-web-sdk": "npm:^1.13.2" - "@grafana/schema": "npm:12.1.0-pre" + "@grafana/schema": "npm:12.2.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@rollup/plugin-node-resolve": "npm:16.0.1" "@rollup/plugin-terser": "npm:0.4.4" "@testing-library/dom": "npm:10.4.0" @@ -3655,7 +3655,7 @@ __metadata: languageName: node linkType: hard -"@grafana/schema@npm:12.1.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": +"@grafana/schema@npm:12.2.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": version: 0.0.0-use.local resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: @@ -3672,18 +3672,18 @@ __metadata: languageName: unknown linkType: soft -"@grafana/sql@npm:12.1.0-pre, @grafana/sql@workspace:*, @grafana/sql@workspace:packages/grafana-sql": +"@grafana/sql@npm:12.2.0-pre, @grafana/sql@workspace:*, @grafana/sql@workspace:packages/grafana-sql": version: 0.0.0-use.local resolution: "@grafana/sql@workspace:packages/grafana-sql" dependencies: "@emotion/css": "npm:11.13.5" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" - "@grafana/i18n": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" + "@grafana/i18n": "npm:12.2.0-pre" "@grafana/plugin-ui": "npm:0.10.7" - "@grafana/runtime": "npm:12.1.0-pre" + "@grafana/runtime": "npm:12.2.0-pre" "@grafana/tsconfig": "npm:^2.0.0" - "@grafana/ui": "npm:12.1.0-pre" + "@grafana/ui": "npm:12.2.0-pre" "@react-awesome-query-builder/ui": "npm:6.6.15" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:^6.1.2" @@ -3736,7 +3736,7 @@ __metadata: languageName: node linkType: hard -"@grafana/ui@npm:12.1.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": +"@grafana/ui@npm:12.2.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: @@ -3746,11 +3746,11 @@ __metadata: "@emotion/serialize": "npm:1.3.3" "@faker-js/faker": "npm:^9.0.0" "@floating-ui/react": "npm:0.27.13" - "@grafana/data": "npm:12.1.0-pre" - "@grafana/e2e-selectors": "npm:12.1.0-pre" + "@grafana/data": "npm:12.2.0-pre" + "@grafana/e2e-selectors": "npm:12.2.0-pre" "@grafana/faro-web-sdk": "npm:^1.13.2" - "@grafana/i18n": "npm:12.1.0-pre" - "@grafana/schema": "npm:12.1.0-pre" + "@grafana/i18n": "npm:12.2.0-pre" + "@grafana/schema": "npm:12.2.0-pre" "@grafana/tsconfig": "npm:^2.0.0" "@hello-pangea/dnd": "npm:18.0.1" "@monaco-editor/react": "npm:4.7.0" From 16339b07887f109dc6f16ebe11e1a1fdb232fb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Fri, 25 Jul 2025 06:28:59 +0200 Subject: [PATCH 085/101] Provisioning: Move repository file API (#108514) * Bump nanogit version * Embed git repository in the Github one --- go.mod | 2 +- go.sum | 4 +- pkg/apis/provisioning/v0alpha1/types.go | 1 + .../v0alpha1/zz_generated.openapi.go | 4 +- pkg/registry/apis/provisioning/files.go | 29 +- .../apis/provisioning/jobs/sync/sync_test.go | 5 + pkg/registry/apis/provisioning/register.go | 9 + .../repository/git/git_repository_mock.go | 50 ++ .../provisioning/repository/git/repository.go | 62 +++ .../repository/git/repository_test.go | 526 +++++++++++++++++- .../github/github_repository_mock.go | 50 ++ .../repository/github/repository.go | 63 +-- .../repository/github/repository_test.go | 147 ++++- .../provisioning/repository/local/local.go | 60 ++ .../repository/local/local_test.go | 284 ++++++++++ .../provisioning/repository/repository.go | 3 + .../repository/repository_mock.go | 18 + .../repository/staged_repository_mock.go | 50 ++ .../apis/provisioning/resources/dualwriter.go | 229 +++++++- .../provisioning/resources/dualwriter_test.go | 147 +++++ .../provisioning.grafana.app-v0alpha1.json | 27 +- pkg/tests/apis/provisioning/helper_test.go | 44 ++ .../apis/provisioning/provisioning_test.go | 284 ++++++++++ .../provisioning/v0alpha1/endpoints.gen.ts | 12 +- 24 files changed, 2011 insertions(+), 99 deletions(-) create mode 100644 pkg/registry/apis/provisioning/resources/dualwriter_test.go diff --git a/go.mod b/go.mod index 8bd1230bdc8..023caf5199a 100644 --- a/go.mod +++ b/go.mod @@ -104,7 +104,7 @@ require ( github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group github.com/grafana/grafana-plugin-sdk-go v0.278.0 // @grafana/plugins-platform-backend github.com/grafana/loki/v3 v3.2.1 // @grafana/observability-logs - github.com/grafana/nanogit v0.0.0-20250717084510-7027b3f0138e // @grafana-app-platform-squad + github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 // @grafana-app-platform-squad github.com/grafana/otel-profiling-go v0.5.1 // @grafana/grafana-backend-group github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // @grafana/observability-traces-and-profiling github.com/grafana/pyroscope/api v1.2.1-0.20250415190842-3ff7247547ae // @grafana/observability-traces-and-profiling diff --git a/go.sum b/go.sum index 2eaadc3e257..11e8025c482 100644 --- a/go.sum +++ b/go.sum @@ -1642,8 +1642,8 @@ github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608 h1:ZYk42718k github.com/grafana/loki/pkg/push v0.0.0-20231124142027-e52380921608/go.mod h1:f3JSoxBTPXX5ec4FxxeC19nTBSxoTz+cBgS3cYLMcr0= github.com/grafana/loki/v3 v3.2.1 h1:VB7u+KHfvL5aHAxgoVBvz5wVhsdGuqKC7uuOFOOe7jw= github.com/grafana/loki/v3 v3.2.1/go.mod h1:WvdLl6wOS+yahaeQY+xhD2m2XzkHDfKr5FZaX7D/X2Y= -github.com/grafana/nanogit v0.0.0-20250717084510-7027b3f0138e h1:IcrC8SqJcNbGlNq0nqptJ4X8pyy21smMaFrhA7DrfYg= -github.com/grafana/nanogit v0.0.0-20250717084510-7027b3f0138e/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= +github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0 h1:cS0SlJGIlZbmDLctNj5vIYGemrJDLy25wwoiIyZWVN8= +github.com/grafana/nanogit v0.0.0-20250723104447-68f58f5ecec0/go.mod h1:ToqLjIdvV3AZQa3K6e5m9hy/nsGaUByc2dWQlctB9iA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/prometheus-alertmanager v0.25.1-0.20250620093340-be61a673dee6 h1:oJnbhG6ZNy10AjsgNeAtAKeGHogIGOMfAsBH6fYYa5M= diff --git a/pkg/apis/provisioning/v0alpha1/types.go b/pkg/apis/provisioning/v0alpha1/types.go index 5ea21495add..edcf2f790fa 100644 --- a/pkg/apis/provisioning/v0alpha1/types.go +++ b/pkg/apis/provisioning/v0alpha1/types.go @@ -295,6 +295,7 @@ const ( ResourceActionCreate ResourceAction = "create" ResourceActionUpdate ResourceAction = "update" ResourceActionDelete ResourceAction = "delete" + ResourceActionMove ResourceAction = "move" ) // This is a container type for any resource type diff --git a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go index 737d2cc29c0..37db6def7d7 100644 --- a/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/provisioning/v0alpha1/zz_generated.openapi.go @@ -1756,10 +1756,10 @@ func schema_pkg_apis_provisioning_v0alpha1_ResourceObjects(ref common.ReferenceC }, "action": { SchemaProps: spec.SchemaProps{ - Description: "The action required/used for dryRun\n\nPossible enum values:\n - `\"create\"`\n - `\"delete\"`\n - `\"update\"`", + Description: "The action required/used for dryRun\n\nPossible enum values:\n - `\"create\"`\n - `\"delete\"`\n - `\"move\"`\n - `\"update\"`", Type: []string{"string"}, Format: "", - Enum: []interface{}{"create", "delete", "update"}, + Enum: []interface{}{"create", "delete", "move", "update"}, }, }, "dryRun": { diff --git a/pkg/registry/apis/provisioning/files.go b/pkg/registry/apis/provisioning/files.go index a0548e1055b..4dd6604c989 100644 --- a/pkg/registry/apis/provisioning/files.go +++ b/pkg/registry/apis/provisioning/files.go @@ -93,9 +93,10 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime. return WithTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() opts := resources.DualWriteOptions{ - Ref: query.Get("ref"), - Message: query.Get("message"), - SkipDryRun: query.Get("skipDryRun") == "true", + Ref: query.Get("ref"), + Message: query.Get("message"), + SkipDryRun: query.Get("skipDryRun") == "true", + OriginalPath: query.Get("originalPath"), } logger := logger.With("url", r.URL.Path, "ref", opts.Ref, "message", opts.Message) ctx := logging.Context(r.Context(), logger) @@ -139,7 +140,24 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime. } obj = resource.AsResourceWrapper() case http.MethodPost: - if isDir { + // Check if this is a move operation first (originalPath query parameter is present) + if opts.OriginalPath != "" { + // For move operations, only read body for file moves (not directory moves) + if !isDir { + opts.Data, err = readBody(r, filesMaxBodySize) + if err != nil { + responder.Error(err) + return + } + } + + resource, err := dualReadWriter.MoveResource(ctx, opts) + if err != nil { + responder.Error(err) + return + } + obj = resource.AsResourceWrapper() + } else if isDir { obj, err = dualReadWriter.CreateFolder(ctx, opts) } else { opts.Data, err = readBody(r, filesMaxBodySize) @@ -148,7 +166,8 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime. return } - resource, err := dualReadWriter.CreateResource(ctx, opts) + var resource *resources.ParsedResource + resource, err = dualReadWriter.CreateResource(ctx, opts) if err != nil { responder.Error(err) return diff --git a/pkg/registry/apis/provisioning/jobs/sync/sync_test.go b/pkg/registry/apis/provisioning/jobs/sync/sync_test.go index 943e32da35c..f7bb4e0c961 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/sync_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/sync_test.go @@ -33,6 +33,11 @@ func (m *mockReaderWriter) CompareFiles(ctx context.Context, base, ref string) ( return m.MockVersioned.CompareFiles(ctx, base, ref) } +func (m *mockReaderWriter) Move(ctx context.Context, oldPath, newPath, ref, message string) error { + args := m.MockRepository.Called(ctx, oldPath, newPath, ref, message) + return args.Error(0) +} + func TestSyncer_Sync(t *testing.T) { tests := []struct { name string diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index e3209fa7026..9b60c22df44 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -794,6 +794,15 @@ func (b *APIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, err Required: false, }, }, + { + ParameterProps: spec3.ParameterProps{ + Name: "originalPath", + In: "query", + Description: "path of file to move (used with POST method for move operations). Must be same type as target path: file-to-file (e.g., 'some/a.json' -> 'c/d.json') or folder-to-folder (e.g., 'some/' -> 'new/')", + Schema: spec.StringProperty(), + Required: false, + }, + }, } sub.Delete.Parameters = comment sub.Post.Parameters = comment diff --git a/pkg/registry/apis/provisioning/repository/git/git_repository_mock.go b/pkg/registry/apis/provisioning/repository/git/git_repository_mock.go index d16a5450326..4b914b50f19 100644 --- a/pkg/registry/apis/provisioning/repository/git/git_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/git/git_repository_mock.go @@ -451,6 +451,56 @@ func (_c *MockGitRepository_ListRefs_Call) RunAndReturn(run func(context.Context return _c } +// Move provides a mock function with given fields: ctx, oldPath, newPath, ref, message +func (_m *MockGitRepository) Move(ctx context.Context, oldPath string, newPath string, ref string, message string) error { + ret := _m.Called(ctx, oldPath, newPath, ref, message) + + if len(ret) == 0 { + panic("no return value specified for Move") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok { + r0 = rf(ctx, oldPath, newPath, ref, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockGitRepository_Move_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Move' +type MockGitRepository_Move_Call struct { + *mock.Call +} + +// Move is a helper method to define mock.On call +// - ctx context.Context +// - oldPath string +// - newPath string +// - ref string +// - message string +func (_e *MockGitRepository_Expecter) Move(ctx interface{}, oldPath interface{}, newPath interface{}, ref interface{}, message interface{}) *MockGitRepository_Move_Call { + return &MockGitRepository_Move_Call{Call: _e.mock.On("Move", ctx, oldPath, newPath, ref, message)} +} + +func (_c *MockGitRepository_Move_Call) Run(run func(ctx context.Context, oldPath string, newPath string, ref string, message string)) *MockGitRepository_Move_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockGitRepository_Move_Call) Return(_a0 error) *MockGitRepository_Move_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockGitRepository_Move_Call) RunAndReturn(run func(context.Context, string, string, string, string) error) *MockGitRepository_Move_Call { + _c.Call.Return(run) + return _c +} + // OnCreate provides a mock function with given fields: ctx func (_m *MockGitRepository) OnCreate(ctx context.Context) ([]map[string]interface{}, error) { ret := _m.Called(ctx) diff --git a/pkg/registry/apis/provisioning/repository/git/repository.go b/pkg/registry/apis/provisioning/repository/git/repository.go index a822b20eb8d..5bcdb12cd94 100644 --- a/pkg/registry/apis/provisioning/repository/git/repository.go +++ b/pkg/registry/apis/provisioning/repository/git/repository.go @@ -457,6 +457,30 @@ func (r *gitRepository) Delete(ctx context.Context, path, ref, comment string) e return r.commitAndPush(ctx, writer, comment) } +func (r *gitRepository) Move(ctx context.Context, oldPath, newPath, ref, comment string) error { + if ref == "" { + ref = r.gitConfig.Branch + } + ctx, _ = r.logger(ctx, ref) + + branchRef, err := r.ensureBranchExists(ctx, ref) + if err != nil { + return err + } + + // Create a staged writer + writer, err := r.client.NewStagedWriter(ctx, branchRef) + if err != nil { + return fmt.Errorf("create staged writer: %w", err) + } + + if err := r.move(ctx, oldPath, newPath, writer); err != nil { + return err + } + + return r.commitAndPush(ctx, writer, comment) +} + func (r *gitRepository) delete(ctx context.Context, path string, writer nanogit.StagedWriter) error { finalPath := safepath.Join(r.gitConfig.Path, path) // Check if it's a directory - use DeleteTree for directories, DeleteBlob for files @@ -480,6 +504,44 @@ func (r *gitRepository) delete(ctx context.Context, path string, writer nanogit. return nil } +func (r *gitRepository) move(ctx context.Context, oldPath, newPath string, writer nanogit.StagedWriter) error { + oldFinalPath := safepath.Join(r.gitConfig.Path, oldPath) + newFinalPath := safepath.Join(r.gitConfig.Path, newPath) + + // Check if moving directories + if safepath.IsDir(oldPath) && safepath.IsDir(newPath) { + // For directories, trim trailing slashes and use MoveTree + oldTrimmed := strings.TrimSuffix(oldFinalPath, "/") + newTrimmed := strings.TrimSuffix(newFinalPath, "/") + + if _, err := writer.MoveTree(ctx, oldTrimmed, newTrimmed); err != nil { + if errors.Is(err, nanogit.ErrObjectNotFound) { + return repository.ErrFileNotFound + } + if errors.Is(err, nanogit.ErrObjectAlreadyExists) { + return repository.ErrFileAlreadyExists + } + return fmt.Errorf("move tree: %w", err) + } + } else if !safepath.IsDir(oldPath) && !safepath.IsDir(newPath) { + // For files, use MoveBlob operation + if _, err := writer.MoveBlob(ctx, oldFinalPath, newFinalPath); err != nil { + if errors.Is(err, nanogit.ErrObjectNotFound) { + return repository.ErrFileNotFound + } + if errors.Is(err, nanogit.ErrObjectAlreadyExists) { + return repository.ErrFileAlreadyExists + } + return fmt.Errorf("move blob: %w", err) + } + } else { + // Mismatched types (file to directory or vice versa) + return apierrors.NewBadRequest("cannot move between file and directory types") + } + + return nil +} + func (r *gitRepository) History(_ context.Context, _ string, _ string) ([]provisioning.HistoryItem, error) { return nil, &apierrors.StatusError{ErrStatus: metav1.Status{ Status: metav1.StatusFailure, diff --git a/pkg/registry/apis/provisioning/repository/git/repository_test.go b/pkg/registry/apis/provisioning/repository/git/repository_test.go index 2c731b4c9f3..f70346825f6 100644 --- a/pkg/registry/apis/provisioning/repository/git/repository_test.go +++ b/pkg/registry/apis/provisioning/repository/git/repository_test.go @@ -3244,7 +3244,7 @@ func TestGitRepository_CompareFiles_EmptyBase(t *testing.T) { // Verify CompareCommits was called with empty base hash and feature hash require.Equal(t, 1, mockClient.CompareCommitsCallCount()) _, baseHash, refHash := mockClient.CompareCommitsArgsForCall(0) - require.Equal(t, hash.Zero, baseHash) // Empty hash for empty base + require.Equal(t, hash.Zero, baseHash) // Empty base should be zero hash require.Equal(t, hash.MustFromHex("0102030405060708090a0b0c0d0e0f1011121314"), refHash) } @@ -3812,3 +3812,527 @@ func TestGitRepository_OnDelete(t *testing.T) { }) } } + +func TestGitRepository_Move(t *testing.T) { + tests := []struct { + name string + oldPath string + newPath string + ref string + comment string + setupMock func(*mocks.FakeClient) + expectedError string + }{ + { + name: "successful move", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move file", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock ensureBranchExists behavior + refHash, _ := hash.FromHex("1234567890abcdef1234567890abcdef12345678") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + // Mock NewStagedWriter + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + // Mock MoveBlob - returns the hash of the moved blob + movedHash, _ := hash.FromHex("abcdef1234567890abcdef1234567890abcdef12") + mockWriter.MoveBlobReturns(movedHash, nil) + + // Mock commit and push + commitHash, _ := hash.FromHex("fedcba0987654321fedcba0987654321fedcba09") + mockWriter.CommitReturns(&nanogit.Commit{Hash: commitHash}, nil) + mockWriter.PushReturns(nil) + }, + }, + { + name: "move with empty ref uses default branch", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "", + comment: "move file", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock ensureBranchExists behavior for default branch + refHash, _ := hash.FromHex("aaaa1111bbbb2222cccc3333dddd4444eeee5555") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + // Mock NewStagedWriter + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + // Mock MoveBlob + moveHash, _ := hash.FromHex("bbbb2222cccc3333dddd4444eeee5555ffff6666") + mockWriter.MoveBlobReturns(moveHash, nil) + + // Mock commit and push + commitHash, _ := hash.FromHex("cccc3333dddd4444eeee5555ffff6666aaaa1111") + mockWriter.CommitReturns(&nanogit.Commit{Hash: commitHash}, nil) + mockWriter.PushReturns(nil) + }, + }, + { + name: "successful directory move", + oldPath: "old/", + newPath: "new/", + ref: "main", + comment: "move directory", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock ensureBranchExists behavior + refHash, _ := hash.FromHex("dddd4444eeee5555ffff6666aaaa1111bbbb2222") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + // Mock NewStagedWriter + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + // Mock MoveTree (for directories, we trim trailing slashes) + treeHash, _ := hash.FromHex("eeee5555ffff6666aaaa1111bbbb2222cccc3333") + mockWriter.MoveTreeReturns(treeHash, nil) + + // Mock commit and push + commitHash, _ := hash.FromHex("ffff6666aaaa1111bbbb2222cccc3333dddd4444") + mockWriter.CommitReturns(&nanogit.Commit{Hash: commitHash}, nil) + mockWriter.PushReturns(nil) + }, + }, + { + name: "move file to directory type should fail", + oldPath: "file.yaml", + newPath: "directory/", + ref: "main", + comment: "move file to directory", + expectedError: "cannot move between file and directory types", + setupMock: func(mockClient *mocks.FakeClient) { + // No mocks needed as this should fail early during validation + }, + }, + { + name: "move non-existent file", + oldPath: "nonexistent.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move missing file", + expectedError: "file not found", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock ensureBranchExists behavior + refHash, _ := hash.FromHex("aaaa0000bbbb1111cccc2222dddd3333eeee4444") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + // Mock NewStagedWriter + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + // Mock MoveBlob to return not found error + mockWriter.MoveBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound) + }, + }, + { + name: "move to existing file should fail", + oldPath: "old.yaml", + newPath: "existing.yaml", + ref: "main", + comment: "move to existing", + expectedError: "file already exists", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock ensureBranchExists behavior + refHash, _ := hash.FromHex("ffff0000eeee1111dddd2222cccc3333bbbb4444") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + // Mock NewStagedWriter + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + // Mock MoveBlob to return already exists error + mockWriter.MoveBlobReturns(hash.Hash{}, nanogit.ErrObjectAlreadyExists) + }, + }, + { + name: "branch creation fails", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "nonexistent-branch", + comment: "move on nonexistent branch", + expectedError: "get source branch ref", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock branch not found + mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound) + // Mock getting source branch for creation - also fails + mockClient.GetRefReturnsOnCall(1, nanogit.Ref{}, nanogit.ErrObjectNotFound) + }, + }, + { + name: "staged writer creation fails", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move file", + expectedError: "create staged writer", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock ensureBranchExists behavior + refHash, _ := hash.FromHex("1111aaaa2222bbbb3333cccc4444dddd5555eeee") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + // Mock NewStagedWriter failure + mockClient.NewStagedWriterReturns(nil, errors.New("writer creation failed")) + }, + }, + { + name: "commit fails", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move file", + expectedError: "commit changes", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock ensureBranchExists behavior + refHash, _ := hash.FromHex("2222bbbb3333cccc4444dddd5555eeee6666ffff") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + // Mock NewStagedWriter + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + // Mock MoveBlob success + moveHash, _ := hash.FromHex("3333cccc4444dddd5555eeee6666ffff7777aaaa") + mockWriter.MoveBlobReturns(moveHash, nil) + + // Mock commit failure + mockWriter.CommitReturns(&nanogit.Commit{}, errors.New("commit failed")) + }, + }, + { + name: "push fails", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move file", + expectedError: "push changes", + setupMock: func(mockClient *mocks.FakeClient) { + // Mock ensureBranchExists behavior + refHash, _ := hash.FromHex("4444dddd5555eeee6666ffff7777aaaa8888bbbb") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + // Mock NewStagedWriter + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + + // Mock MoveBlob success + moveHash, _ := hash.FromHex("5555eeee6666ffff7777aaaa8888bbbb9999cccc") + mockWriter.MoveBlobReturns(moveHash, nil) + + // Mock commit success + commitHash, _ := hash.FromHex("6666ffff7777aaaa8888bbbb9999cccc0000dddd") + mockWriter.CommitReturns(&nanogit.Commit{Hash: commitHash}, nil) + + // Mock push failure + mockWriter.PushReturns(errors.New("push failed")) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup mock + mockClient := &mocks.FakeClient{} + tt.setupMock(mockClient) + + // Create repository config + config := &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitRepositoryType, + }, + } + + gitConfig := RepositoryConfig{ + URL: "https://github.com/example/repo.git", + Branch: "main", + Token: "token123", + Path: "configs", + } + + gitRepo := &gitRepository{ + config: config, + gitConfig: gitConfig, + client: mockClient, + } + + // Execute move operation + err := gitRepo.Move(context.Background(), tt.oldPath, tt.newPath, tt.ref, tt.comment) + + // Verify results + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestGitRepository_Move_ErrorConditions(t *testing.T) { + tests := []struct { + name string + oldPath string + newPath string + ref string + comment string + setupMock func(*mocks.FakeClient) + expectedError string + errorType error + }{ + { + name: "MoveBlob - ErrObjectNotFound", + oldPath: "missing.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move missing file", + setupMock: func(mockClient *mocks.FakeClient) { + refHash, _ := hash.FromHex("1234567890abcdef1234567890abcdef12345678") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + mockWriter.MoveBlobReturns(hash.Hash{}, nanogit.ErrObjectNotFound) + }, + expectedError: "file not found", + errorType: repository.ErrFileNotFound, + }, + { + name: "MoveBlob - ErrObjectAlreadyExists", + oldPath: "old.yaml", + newPath: "existing.yaml", + ref: "main", + comment: "move to existing file", + setupMock: func(mockClient *mocks.FakeClient) { + refHash, _ := hash.FromHex("abcdef1234567890abcdef1234567890abcdef12") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + mockWriter.MoveBlobReturns(hash.Hash{}, nanogit.ErrObjectAlreadyExists) + }, + expectedError: "file already exists", + errorType: repository.ErrFileAlreadyExists, + }, + { + name: "MoveBlob - generic error", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move file with generic error", + setupMock: func(mockClient *mocks.FakeClient) { + refHash, _ := hash.FromHex("fedcba0987654321fedcba0987654321fedcba09") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + mockWriter.MoveBlobReturns(hash.Hash{}, errors.New("network error")) + }, + expectedError: "move blob: network error", + }, + { + name: "MoveTree - ErrObjectNotFound", + oldPath: "missing-dir/", + newPath: "new-dir/", + ref: "main", + comment: "move missing directory", + setupMock: func(mockClient *mocks.FakeClient) { + refHash, _ := hash.FromHex("1111222233334444555566667777888899990000") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + mockWriter.MoveTreeReturns(hash.Hash{}, nanogit.ErrObjectNotFound) + }, + expectedError: "file not found", + errorType: repository.ErrFileNotFound, + }, + { + name: "MoveTree - ErrObjectAlreadyExists", + oldPath: "old-dir/", + newPath: "existing-dir/", + ref: "main", + comment: "move to existing directory", + setupMock: func(mockClient *mocks.FakeClient) { + refHash, _ := hash.FromHex("aaaa1111bbbb2222cccc3333dddd4444eeee5555") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + mockWriter.MoveTreeReturns(hash.Hash{}, nanogit.ErrObjectAlreadyExists) + }, + expectedError: "file already exists", + errorType: repository.ErrFileAlreadyExists, + }, + { + name: "MoveTree - generic error", + oldPath: "old-dir/", + newPath: "new-dir/", + ref: "main", + comment: "move directory with generic error", + setupMock: func(mockClient *mocks.FakeClient) { + refHash, _ := hash.FromHex("ffff6666eeee5555dddd4444cccc3333bbbb2222") + mockClient.GetRefReturns(nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + + mockWriter := &mocks.FakeStagedWriter{} + mockClient.NewStagedWriterReturns(mockWriter, nil) + mockWriter.MoveTreeReturns(hash.Hash{}, errors.New("permission denied")) + }, + expectedError: "move tree: permission denied", + }, + { + name: "invalid branch name", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "invalid//branch", + comment: "move with invalid branch", + setupMock: func(mockClient *mocks.FakeClient) { + // No mock setup needed as error is caught during branch validation + }, + expectedError: "invalid branch name", + }, + { + name: "ensure branch exists fails - source branch not found", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "new-branch", + comment: "move to new branch when source doesn't exist", + setupMock: func(mockClient *mocks.FakeClient) { + // First call - new branch doesn't exist + mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound) + // Second call - source branch also doesn't exist + mockClient.GetRefReturnsOnCall(1, nanogit.Ref{}, nanogit.ErrObjectNotFound) + }, + expectedError: "get source branch ref", + }, + { + name: "ensure branch exists fails - branch creation error", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "new-branch", + comment: "move to new branch with creation failure", + setupMock: func(mockClient *mocks.FakeClient) { + // First call - new branch doesn't exist + mockClient.GetRefReturnsOnCall(0, nanogit.Ref{}, nanogit.ErrObjectNotFound) + // Second call - get source branch succeeds + refHash, _ := hash.FromHex("7777aaaa8888bbbb9999cccc0000dddd1111eeee") + mockClient.GetRefReturnsOnCall(1, nanogit.Ref{ + Name: "refs/heads/main", + Hash: refHash, + }, nil) + // CreateRef fails + mockClient.CreateRefReturns(errors.New("create ref failed")) + }, + expectedError: "create branch", + }, + { + name: "directory to file move type mismatch", + oldPath: "directory/", + newPath: "file.yaml", + ref: "main", + comment: "move directory to file", + setupMock: func(mockClient *mocks.FakeClient) { + // No mock setup needed as error is caught during validation + }, + expectedError: "cannot move between file and directory types", + }, + { + name: "file to directory move type mismatch", + oldPath: "file.yaml", + newPath: "directory/", + ref: "main", + comment: "move file to directory", + setupMock: func(mockClient *mocks.FakeClient) { + // No mock setup needed as error is caught during validation + }, + expectedError: "cannot move between file and directory types", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockClient := &mocks.FakeClient{} + tt.setupMock(mockClient) + + config := &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitRepositoryType, + }, + } + + gitConfig := RepositoryConfig{ + URL: "https://github.com/example/repo.git", + Branch: "main", + Token: "token123", + Path: "configs", + } + + gitRepo := &gitRepository{ + config: config, + gitConfig: gitConfig, + client: mockClient, + } + + err := gitRepo.Move(context.Background(), tt.oldPath, tt.newPath, tt.ref, tt.comment) + + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + + if tt.errorType != nil { + require.ErrorIs(t, err, tt.errorType) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go b/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go index a2c2798819a..5ccd5d2e319 100644 --- a/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/github/github_repository_mock.go @@ -453,6 +453,56 @@ func (_c *MockGithubRepository_ListRefs_Call) RunAndReturn(run func(context.Cont return _c } +// Move provides a mock function with given fields: ctx, oldPath, newPath, ref, message +func (_m *MockGithubRepository) Move(ctx context.Context, oldPath string, newPath string, ref string, message string) error { + ret := _m.Called(ctx, oldPath, newPath, ref, message) + + if len(ret) == 0 { + panic("no return value specified for Move") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok { + r0 = rf(ctx, oldPath, newPath, ref, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockGithubRepository_Move_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Move' +type MockGithubRepository_Move_Call struct { + *mock.Call +} + +// Move is a helper method to define mock.On call +// - ctx context.Context +// - oldPath string +// - newPath string +// - ref string +// - message string +func (_e *MockGithubRepository_Expecter) Move(ctx interface{}, oldPath interface{}, newPath interface{}, ref interface{}, message interface{}) *MockGithubRepository_Move_Call { + return &MockGithubRepository_Move_Call{Call: _e.mock.On("Move", ctx, oldPath, newPath, ref, message)} +} + +func (_c *MockGithubRepository_Move_Call) Run(run func(ctx context.Context, oldPath string, newPath string, ref string, message string)) *MockGithubRepository_Move_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockGithubRepository_Move_Call) Return(_a0 error) *MockGithubRepository_Move_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockGithubRepository_Move_Call) RunAndReturn(run func(context.Context, string, string, string, string) error) *MockGithubRepository_Move_Call { + _c.Call.Return(run) + return _c +} + // OnCreate provides a mock function with given fields: ctx func (_m *MockGithubRepository) OnCreate(ctx context.Context) ([]map[string]interface{}, error) { ret := _m.Called(ctx) diff --git a/pkg/registry/apis/provisioning/repository/github/repository.go b/pkg/registry/apis/provisioning/repository/github/repository.go index e6cc5aa304d..d2da1de41d1 100644 --- a/pkg/registry/apis/provisioning/repository/github/repository.go +++ b/pkg/registry/apis/provisioning/repository/github/repository.go @@ -22,7 +22,7 @@ const githubTokenSecretSuffix = "-github-token" // Make sure all public functions of this struct call the (*githubRepository).logger function, to ensure the GH repo details are included. type githubRepository struct { - gitRepo git.GitRepository + git.GitRepository config *provisioning.Repository gh Client // assumes github.com base URL secrets secrets.RepositorySecrets @@ -62,19 +62,15 @@ func NewGitHub( } return &githubRepository{ - config: config, - gitRepo: gitRepo, - gh: factory.New(ctx, token), // TODO, baseURL from config - owner: owner, - repo: repo, - secrets: secrets, + config: config, + GitRepository: gitRepo, + gh: factory.New(ctx, token), // TODO, baseURL from config + owner: owner, + repo: repo, + secrets: secrets, }, nil } -func (r *githubRepository) Config() *provisioning.Repository { - return r.gitRepo.Config() -} - func (r *githubRepository) Owner() string { return r.owner } @@ -89,7 +85,7 @@ func (r *githubRepository) Client() Client { // Validate implements provisioning.Repository. func (r *githubRepository) Validate() (list field.ErrorList) { - cfg := r.gitRepo.Config() + cfg := r.Config() gh := cfg.Spec.GitHub if gh == nil { list = append(list, field.Required(field.NewPath("spec", "github"), "a github config is required")) @@ -110,7 +106,7 @@ func (r *githubRepository) Validate() (list field.ErrorList) { return list } - return r.gitRepo.Validate() + return r.GitRepository.Validate() } func ParseOwnerRepoGithub(giturl string) (owner string, repo string, err error) { @@ -139,32 +135,7 @@ func (r *githubRepository) Test(ctx context.Context) (*provisioning.TestResults, field.NewPath("spec", "github", "url"), url, err.Error())), nil } - return r.gitRepo.Test(ctx) -} - -// ReadResource implements provisioning.Repository. -func (r *githubRepository) Read(ctx context.Context, filePath, ref string) (*repository.FileInfo, error) { - return r.gitRepo.Read(ctx, filePath, ref) -} - -func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]repository.FileTreeEntry, error) { - return r.gitRepo.ReadTree(ctx, ref) -} - -func (r *githubRepository) Create(ctx context.Context, path, ref string, data []byte, comment string) error { - return r.gitRepo.Create(ctx, path, ref, data, comment) -} - -func (r *githubRepository) Update(ctx context.Context, path, ref string, data []byte, comment string) error { - return r.gitRepo.Update(ctx, path, ref, data, comment) -} - -func (r *githubRepository) Write(ctx context.Context, path string, ref string, data []byte, message string) error { - return r.gitRepo.Write(ctx, path, ref, data, message) -} - -func (r *githubRepository) Delete(ctx context.Context, path, ref, comment string) error { - return r.gitRepo.Delete(ctx, path, ref, comment) + return r.GitRepository.Test(ctx) } func (r *githubRepository) History(ctx context.Context, path, ref string) ([]provisioning.HistoryItem, error) { @@ -214,7 +185,7 @@ func (r *githubRepository) History(ctx context.Context, path, ref string) ([]pro // ListRefs list refs from the git repository and add the ref URL to the ref item func (r *githubRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem, error) { - refs, err := r.gitRepo.ListRefs(ctx) + refs, err := r.GitRepository.ListRefs(ctx) if err != nil { return nil, fmt.Errorf("list refs: %w", err) } @@ -226,14 +197,6 @@ func (r *githubRepository) ListRefs(ctx context.Context) ([]provisioning.RefItem return refs, nil } -func (r *githubRepository) LatestRef(ctx context.Context) (string, error) { - return r.gitRepo.LatestRef(ctx) -} - -func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ([]repository.VersionedFileChange, error) { - return r.gitRepo.CompareFiles(ctx, base, ref) -} - // ResourceURLs implements RepositoryWithURLs. func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.FileInfo) (*provisioning.ResourceURLs, error) { cfg := r.config.Spec.GitHub @@ -261,10 +224,6 @@ func (r *githubRepository) ResourceURLs(ctx context.Context, file *repository.Fi return urls, nil } -func (r *githubRepository) Stage(ctx context.Context, opts repository.StageOptions) (repository.StagedRepository, error) { - return r.gitRepo.Stage(ctx, opts) -} - func (r *githubRepository) OnCreate(_ context.Context) ([]map[string]interface{}, error) { return nil, nil } diff --git a/pkg/registry/apis/provisioning/repository/github/repository_test.go b/pkg/registry/apis/provisioning/repository/github/repository_test.go index 960b80e42d0..962a7a4e2b1 100644 --- a/pkg/registry/apis/provisioning/repository/github/repository_test.go +++ b/pkg/registry/apis/provisioning/repository/github/repository_test.go @@ -105,7 +105,7 @@ func TestNewGitHub(t *testing.T) { assert.Equal(t, tt.expectedRepo, repo.Repo()) concreteRepo, ok := repo.(*githubRepository) require.True(t, ok) - assert.Equal(t, gitRepo, concreteRepo.gitRepo) + assert.Equal(t, gitRepo, concreteRepo.GitRepository) } }) } @@ -301,8 +301,8 @@ func TestGitHubRepositoryValidate(t *testing.T) { } repo := &githubRepository{ - config: tt.config, - gitRepo: mockGitRepo, + config: tt.config, + GitRepository: mockGitRepo, } errors := repo.Validate() @@ -388,10 +388,10 @@ func TestGitHubRepositoryTest(t *testing.T) { } repo := &githubRepository{ - config: tt.config, - gitRepo: mockGitRepo, - owner: "grafana", - repo: "grafana", + config: tt.config, + GitRepository: mockGitRepo, + owner: "grafana", + repo: "grafana", } result, err := repo.Test(context.Background()) @@ -802,8 +802,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("Config").Return(config) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } result := repo.Config() @@ -822,8 +822,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("Read", ctx, "test.yaml", "main").Return(expectedFileInfo, nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } result, err := repo.Read(ctx, "test.yaml", "main") @@ -840,8 +840,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("ReadTree", ctx, "main").Return(expectedEntries, nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } result, err := repo.ReadTree(ctx, "main") @@ -856,8 +856,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("Create", ctx, "new-file.yaml", "main", data, "Create new file").Return(nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } err := repo.Create(ctx, "new-file.yaml", "main", data, "Create new file") @@ -871,8 +871,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("Update", ctx, "existing-file.yaml", "main", data, "Update file").Return(nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } err := repo.Update(ctx, "existing-file.yaml", "main", data, "Update file") @@ -886,8 +886,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("Write", ctx, "file.yaml", "main", data, "Write file").Return(nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } err := repo.Write(ctx, "file.yaml", "main", data, "Write file") @@ -900,8 +900,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("Delete", ctx, "file.yaml", "main", "Delete file").Return(nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } err := repo.Delete(ctx, "file.yaml", "main", "Delete file") @@ -915,8 +915,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("LatestRef", ctx).Return(expectedRef, nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } result, err := repo.LatestRef(ctx) @@ -935,8 +935,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("ListRefs", ctx).Return(gitRepoRefs, nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } result, err := repo.ListRefs(ctx) @@ -971,8 +971,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("CompareFiles", ctx, "main", "feature-branch").Return(expectedChanges, nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } result, err := repo.CompareFiles(ctx, "main", "feature-branch") @@ -991,8 +991,8 @@ func TestGitHubRepositoryDelegation(t *testing.T) { mockGitRepo.On("Stage", ctx, opts).Return(mockStagedRepo, nil) repo := &githubRepository{ - config: config, - gitRepo: mockGitRepo, + config: config, + GitRepository: mockGitRepo, } result, err := repo.Stage(ctx, opts) @@ -1126,3 +1126,92 @@ func TestGitHubRepository_OnDelete(t *testing.T) { }) } } + +func TestGithubRepository_Move(t *testing.T) { + tests := []struct { + name string + oldPath string + newPath string + ref string + comment string + setupMock func(*git.MockGitRepository) + expectedErr error + }{ + { + name: "successful move delegates to git repository", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move file", + setupMock: func(mockGitRepo *git.MockGitRepository) { + mockGitRepo.EXPECT().Move(context.Background(), "old.yaml", "new.yaml", "main", "move file").Return(nil) + }, + expectedErr: nil, + }, + { + name: "move error from git repository", + oldPath: "old.yaml", + newPath: "new.yaml", + ref: "main", + comment: "move file", + setupMock: func(mockGitRepo *git.MockGitRepository) { + mockGitRepo.EXPECT().Move(context.Background(), "old.yaml", "new.yaml", "main", "move file").Return(errors.New("git move failed")) + }, + expectedErr: errors.New("git move failed"), + }, + { + name: "successful directory move", + oldPath: "old/", + newPath: "new/", + ref: "main", + comment: "move directory", + setupMock: func(mockGitRepo *git.MockGitRepository) { + mockGitRepo.EXPECT().Move(context.Background(), "old/", "new/", "main", "move directory").Return(nil) + }, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mock git repository + mockGitRepo := git.NewMockGitRepository(t) + mockSecrets := &secrets.MockRepositorySecrets{} + + // Setup mock expectations + tt.setupMock(mockGitRepo) + + // Create GitHub repository + config := &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitHubRepositoryType, + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "https://github.com/example/repo", + }, + }, + } + + githubRepo := &githubRepository{ + config: config, + GitRepository: mockGitRepo, + owner: "example", + repo: "repo", + secrets: mockSecrets, + } + + // Execute move operation + err := githubRepo.Move(context.Background(), tt.oldPath, tt.newPath, tt.ref, tt.comment) + + // Verify results + if tt.expectedErr != nil { + require.Error(t, err) + assert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/local/local.go b/pkg/registry/apis/provisioning/repository/local/local.go index 5f63b83cfa1..0ab620e6639 100644 --- a/pkg/registry/apis/provisioning/repository/local/local.go +++ b/pkg/registry/apis/provisioning/repository/local/local.go @@ -372,3 +372,63 @@ func (r *localRepository) Delete(ctx context.Context, path string, ref string, c return os.Remove(fullPath) } + +func (r *localRepository) Move(ctx context.Context, oldPath, newPath, ref, comment string) error { + if err := r.validateRequest(ref); err != nil { + return err + } + + oldFullPath := safepath.Join(r.path, oldPath) + newFullPath := safepath.Join(r.path, newPath) + + // Check if source exists + sourceInfo, err := os.Stat(oldFullPath) + if errors.Is(err, os.ErrNotExist) { + return repository.ErrFileNotFound + } else if err != nil { + return fmt.Errorf("check source: %w", err) + } + + // Check if destination already exists + if _, err := os.Stat(newFullPath); !errors.Is(err, os.ErrNotExist) { + if err != nil { + return fmt.Errorf("check destination: %w", err) + } + return repository.ErrFileAlreadyExists + } + + // Validate move types + sourceIsDir := sourceInfo.IsDir() + targetIsDir := safepath.IsDir(newPath) + + if sourceIsDir != targetIsDir { + return apierrors.NewBadRequest("cannot move between file and directory types") + } + + // Create destination directory if needed + if !sourceIsDir { + // For file moves, create the directory containing the file + destParent := path.Dir(newFullPath) + if err := os.MkdirAll(destParent, 0700); err != nil { + return fmt.Errorf("create destination directory: %w", err) + } + } else { + // For directory moves, create the parent directory of the destination + // but not the destination directory itself (os.Rename will create it) + // We need to be careful with trailing slashes in directory paths + cleanNewPath := strings.TrimSuffix(newFullPath, "/") + destParent := path.Dir(cleanNewPath) + if destParent != "." && destParent != "/" && destParent != r.path { + if err := os.MkdirAll(destParent, 0700); err != nil { + return fmt.Errorf("create destination parent directory: %w", err) + } + } + } + + // Move the file or directory + if err := os.Rename(oldFullPath, newFullPath); err != nil { + return fmt.Errorf("move: %w", err) + } + + return nil +} diff --git a/pkg/registry/apis/provisioning/repository/local/local_test.go b/pkg/registry/apis/provisioning/repository/local/local_test.go index 7895da24995..77a0b99fa09 100644 --- a/pkg/registry/apis/provisioning/repository/local/local_test.go +++ b/pkg/registry/apis/provisioning/repository/local/local_test.go @@ -1517,3 +1517,287 @@ func TestLocalRepository_Config(t *testing.T) { }) } } + +func TestLocalRepository_Move(t *testing.T) { + testCases := []struct { + name string + setup func(t *testing.T) (string, *localRepository) + oldPath string + newPath string + ref string + comment string + expectedErr error + expectedContent string // Expected content of moved file (empty string means don't verify content) + }{ + { + name: "successful move", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + + // Create source file + sourceFile := filepath.Join(tempDir, "source.txt") + err := os.WriteFile(sourceFile, []byte("source content"), 0600) + require.NoError(t, err) + + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + oldPath: "source.txt", + newPath: "destination.txt", + ref: "", + comment: "move file", + expectedErr: nil, + expectedContent: "source content", + }, + { + name: "move to subdirectory", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + + // Create source file + sourceFile := filepath.Join(tempDir, "test.txt") + err := os.WriteFile(sourceFile, []byte("test content"), 0600) + require.NoError(t, err) + + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + oldPath: "test.txt", + newPath: "newdir/moved.txt", + ref: "", + comment: "move to subdir", + expectedErr: nil, + expectedContent: "test content", + }, + { + name: "move non-existent file", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + oldPath: "nonexistent.txt", + newPath: "destination.txt", + ref: "", + comment: "move missing", + expectedErr: repository.ErrFileNotFound, + expectedContent: "", // No content verification for error cases + }, + { + name: "move to existing file", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + + // Create source and destination files + sourceFile := filepath.Join(tempDir, "source2.txt") + destFile := filepath.Join(tempDir, "dest2.txt") + + err := os.WriteFile(sourceFile, []byte("source"), 0600) + require.NoError(t, err) + err = os.WriteFile(destFile, []byte("destination"), 0600) + require.NoError(t, err) + + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + oldPath: "source2.txt", + newPath: "dest2.txt", + ref: "", + comment: "move to existing", + expectedErr: repository.ErrFileAlreadyExists, + expectedContent: "", // No content verification for error cases + }, + { + name: "successful directory move", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + + // Create directory with files + subdir := filepath.Join(tempDir, "subdir") + err := os.MkdirAll(subdir, 0700) + require.NoError(t, err) + + // Add a file inside the directory + err = os.WriteFile(filepath.Join(subdir, "file.txt"), []byte("dir content"), 0600) + require.NoError(t, err) + + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + oldPath: "subdir/", + newPath: "newsubdir/", + ref: "", + comment: "move directory", + expectedErr: nil, + expectedContent: "", // No content verification for directory moves + }, + { + name: "move file to directory type should fail", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + + // Create source file + sourceFile := filepath.Join(tempDir, "file.txt") + err := os.WriteFile(sourceFile, []byte("content"), 0600) + require.NoError(t, err) + + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + oldPath: "file.txt", + newPath: "directory/", + ref: "", + comment: "move file to directory", + expectedErr: apierrors.NewBadRequest("cannot move between file and directory types"), + expectedContent: "", // No content verification for error cases + }, + { + name: "move with ref should fail", + setup: func(t *testing.T) (string, *localRepository) { + tempDir := t.TempDir() + + // Create test file + testFile := filepath.Join(tempDir, "ref_test.txt") + err := os.WriteFile(testFile, []byte("ref test content"), 0600) + require.NoError(t, err) + + repo := &localRepository{ + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + Local: &provisioning.LocalRepositoryConfig{ + Path: tempDir, + }, + }, + }, + resolver: &LocalFolderResolver{ + PermittedPrefixes: []string{tempDir}, + }, + path: tempDir, + } + + return tempDir, repo + }, + oldPath: "ref_test.txt", + newPath: "ref_dest.txt", + ref: "some-ref", + comment: "move with ref", + expectedErr: apierrors.NewBadRequest("local repository does not support ref"), + expectedContent: "", // No content verification for error cases + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Setup test environment + tempDir, repo := tc.setup(t) + + // Execute the move operation + err := repo.Move(context.Background(), tc.oldPath, tc.newPath, tc.ref, tc.comment) + + // Verify results + if tc.expectedErr != nil { + require.Error(t, err) + assert.Equal(t, tc.expectedErr.Error(), err.Error(), "Error message should match expected") + } else { + require.NoError(t, err) + + // Verify source file no longer exists + sourceFullPath := filepath.Join(tempDir, tc.oldPath) + _, err = os.Stat(sourceFullPath) + assert.True(t, errors.Is(err, os.ErrNotExist), "Source file should no longer exist") + + // Verify destination file exists + destFullPath := filepath.Join(tempDir, tc.newPath) + _, err = os.Stat(destFullPath) + require.NoError(t, err, "Destination file should exist") + + // Verify content if expectedContent is specified + if tc.expectedContent != "" { + //nolint:gosec // G304: is only for tests + content, err := os.ReadFile(destFullPath) + require.NoError(t, err) + assert.Equal(t, tc.expectedContent, string(content), "Content should be preserved") + } + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/repository.go b/pkg/registry/apis/provisioning/repository/repository.go index 0e72dabbf43..718660c9fab 100644 --- a/pkg/registry/apis/provisioning/repository/repository.go +++ b/pkg/registry/apis/provisioning/repository/repository.go @@ -114,6 +114,9 @@ type Writer interface { // Delete a file in the remote repository Delete(ctx context.Context, path, ref, message string) error + + // Move a file from one path to another in the remote repository + Move(ctx context.Context, oldPath, newPath, ref, message string) error } type ReaderWriter interface { diff --git a/pkg/registry/apis/provisioning/repository/repository_mock.go b/pkg/registry/apis/provisioning/repository/repository_mock.go index b7f5bda049b..05fc5a3832a 100644 --- a/pkg/registry/apis/provisioning/repository/repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/repository_mock.go @@ -280,6 +280,24 @@ func (_m *MockRepository) Write(ctx context.Context, path string, ref string, da return r0 } +// Move provides a mock function with given fields: ctx, oldPath, newPath, ref, message +func (_m *MockRepository) Move(ctx context.Context, oldPath string, newPath string, ref string, message string) error { + ret := _m.Called(ctx, oldPath, newPath, ref, message) + + if len(ret) == 0 { + panic("no return value specified for Move") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok { + r0 = rf(ctx, oldPath, newPath, ref, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NewMockRepository creates a new instance of MockRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockRepository(t interface { diff --git a/pkg/registry/apis/provisioning/repository/staged_repository_mock.go b/pkg/registry/apis/provisioning/repository/staged_repository_mock.go index 916b6cc678e..85bfb4f3012 100644 --- a/pkg/registry/apis/provisioning/repository/staged_repository_mock.go +++ b/pkg/registry/apis/provisioning/repository/staged_repository_mock.go @@ -170,6 +170,56 @@ func (_c *MockStagedRepository_Delete_Call) RunAndReturn(run func(context.Contex return _c } +// Move provides a mock function with given fields: ctx, oldPath, newPath, ref, message +func (_m *MockStagedRepository) Move(ctx context.Context, oldPath string, newPath string, ref string, message string) error { + ret := _m.Called(ctx, oldPath, newPath, ref, message) + + if len(ret) == 0 { + panic("no return value specified for Move") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok { + r0 = rf(ctx, oldPath, newPath, ref, message) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockStagedRepository_Move_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Move' +type MockStagedRepository_Move_Call struct { + *mock.Call +} + +// Move is a helper method to define mock.On call +// - ctx context.Context +// - oldPath string +// - newPath string +// - ref string +// - message string +func (_e *MockStagedRepository_Expecter) Move(ctx interface{}, oldPath interface{}, newPath interface{}, ref interface{}, message interface{}) *MockStagedRepository_Move_Call { + return &MockStagedRepository_Move_Call{Call: _e.mock.On("Move", ctx, oldPath, newPath, ref, message)} +} + +func (_c *MockStagedRepository_Move_Call) Run(run func(ctx context.Context, oldPath string, newPath string, ref string, message string)) *MockStagedRepository_Move_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockStagedRepository_Move_Call) Return(_a0 error) *MockStagedRepository_Move_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockStagedRepository_Move_Call) RunAndReturn(run func(context.Context, string, string, string, string) error) *MockStagedRepository_Move_Call { + _c.Call.Return(run) + return _c +} + // Push provides a mock function with given fields: ctx func (_m *MockStagedRepository) Push(ctx context.Context) error { ret := _m.Called(ctx) diff --git a/pkg/registry/apis/provisioning/resources/dualwriter.go b/pkg/registry/apis/provisioning/resources/dualwriter.go index 2145285d52e..50efaa13048 100644 --- a/pkg/registry/apis/provisioning/resources/dualwriter.go +++ b/pkg/registry/apis/provisioning/resources/dualwriter.go @@ -28,11 +28,12 @@ type DualReadWriter struct { } type DualWriteOptions struct { - Path string - Ref string - Message string - Data []byte - SkipDryRun bool + Path string + Ref string + Message string + Data []byte + SkipDryRun bool + OriginalPath string // Used for move operations } func NewDualReadWriter(repo repository.ReaderWriter, parser Parser, folders *FolderManager, access authlib.AccessChecker) *DualReadWriter { @@ -278,6 +279,216 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts D return parsed, err } +// MoveResource moves a resource from one path to another in the repository +func (r *DualReadWriter) MoveResource(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) { + if err := repository.IsWriteAllowed(r.repo.Config(), opts.Ref); err != nil { + return nil, err + } + + if opts.OriginalPath == "" { + return nil, fmt.Errorf("originalPath is required for move operations") + } + + // Validate that both paths are either files or directories (consistent types) + // Files should end without '/', directories should end with '/' + sourceIsDir := safepath.IsDir(opts.OriginalPath) + targetIsDir := safepath.IsDir(opts.Path) + if sourceIsDir != targetIsDir { + return nil, fmt.Errorf("cannot move between file and directory types - source is %s, target is %s", + getPathType(sourceIsDir), getPathType(targetIsDir)) + } + + // Handle directory moves separately (no parsing/authorization needed) + if sourceIsDir { + return r.moveDirectory(ctx, opts) + } + + // Handle file moves with parsing and authorization + return r.moveFile(ctx, opts) +} + +func (r *DualReadWriter) moveDirectory(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) { + // For directory moves, we just perform the repository move without parsing + // Always use the provisioning identity when writing + ctx, _, err := identity.WithProvisioningIdentity(ctx, r.repo.Config().Namespace) + if err != nil { + return nil, fmt.Errorf("unable to use provisioning identity: %w", err) + } + + // Perform the move operation in the repository + if err = r.repo.Move(ctx, opts.OriginalPath, opts.Path, opts.Ref, opts.Message); err != nil { + return nil, fmt.Errorf("move directory in repository: %w", err) + } + + // Create a basic parsed resource response for directories + cfg := r.repo.Config() + parsed := &ParsedResource{ + Action: provisioning.ResourceActionMove, + Info: &repository.FileInfo{ + Path: opts.Path, + Ref: opts.Ref, + }, + GVK: schema.GroupVersionKind{ + Group: FolderResource.Group, + Version: FolderResource.Version, + Kind: "Folder", + }, + GVR: FolderResource, + Repo: provisioning.ResourceRepositoryInfo{ + Type: cfg.Spec.Type, + Namespace: cfg.Namespace, + Name: cfg.Name, + Title: cfg.Spec.Title, + }, + } + + // Handle folder management for main branch + if opts.Ref == "" { + // Ensure destination folder path exists + if _, err := r.folders.EnsureFolderPathExist(ctx, opts.Path); err != nil { + return nil, fmt.Errorf("ensure destination folder path exists: %w", err) + } + + // Try to delete the old folder structure from grafana (if it exists) + // This handles cleanup when folders are moved to new locations + oldFolderName, err := r.folders.EnsureFolderPathExist(ctx, opts.OriginalPath) + if err != nil { + return nil, fmt.Errorf("ensure original folder path exists: %w", err) + } + + if oldFolderName != "" { + oldFolder, err := r.folders.GetFolder(ctx, oldFolderName) + if err != nil && !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("get old folder for cleanup: %w", err) + } + + if err == nil { + err = r.folders.Client().Delete(ctx, oldFolder.GetName(), metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("delete old folder from storage: %w", err) + } + } + } + } + + return parsed, nil +} + +func (r *DualReadWriter) moveFile(ctx context.Context, opts DualWriteOptions) (*ParsedResource, error) { + // Read the original file to get its content for parsing and authorization + originalFile, err := r.repo.Read(ctx, opts.OriginalPath, "") + if err != nil { + return nil, fmt.Errorf("read original file: %w", err) + } + + // Parse the original file to check permissions + parsed, err := r.parser.Parse(ctx, originalFile) + if err != nil { + return nil, fmt.Errorf("parse original file: %w", err) + } + + // Authorize delete on the original path + if err = r.authorize(ctx, parsed, utils.VerbDelete); err != nil { + return nil, fmt.Errorf("not authorized to delete original file: %w", err) + } + + // Determine the content to use for the destination + // If new content is provided in opts.Data, use it; otherwise use original content + var destinationData []byte + if len(opts.Data) > 0 { + destinationData = opts.Data + } else { + destinationData = originalFile.Data + } + + // Create new parsed resource with updated path and content + newInfo := &repository.FileInfo{ + Data: destinationData, + Path: opts.Path, + Ref: opts.Ref, + } + + newParsed, err := r.parser.Parse(ctx, newInfo) + if err != nil { + return nil, fmt.Errorf("parse new file: %w", err) + } + + // Make sure the new resource is valid + if !opts.SkipDryRun { + if err := newParsed.DryRun(ctx); err != nil { + logger := logging.FromContext(ctx).With("path", opts.Path, "originalPath", opts.OriginalPath, "name", newParsed.Obj.GetName(), "ref", opts.Ref) + logger.Warn("failed to dry run resource on move", "error", err) + return nil, fmt.Errorf("error running dryRun on moved resource: %w", err) + } + } + + if len(newParsed.Errors) > 0 { + return nil, fmt.Errorf("errors while parsing moved file [%v]", newParsed.Errors) + } + + // Authorize create on the new path + verb := utils.VerbCreate + if newParsed.Action == provisioning.ResourceActionUpdate { + verb = utils.VerbUpdate + } + if err = r.authorize(ctx, newParsed, verb); err != nil { + return nil, fmt.Errorf("not authorized to create new file: %w", err) + } + + data, err := newParsed.ToSaveBytes() + if err != nil { + return nil, err + } + + // Always use the provisioning identity when writing + ctx, _, err = identity.WithProvisioningIdentity(ctx, newParsed.Obj.GetNamespace()) + if err != nil { + return nil, fmt.Errorf("unable to use provisioning identity: %w", err) + } + + // Perform the move operation in the repository + // If we have new content, we need to update the file content as part of the move + if len(opts.Data) > 0 { + // For moves with content updates, we need to delete the old file and create the new one + if err = r.repo.Delete(ctx, opts.OriginalPath, opts.Ref, opts.Message); err != nil { + return nil, fmt.Errorf("delete original file in repository: %w", err) + } + if err = r.repo.Create(ctx, opts.Path, opts.Ref, data, opts.Message); err != nil { + return nil, fmt.Errorf("create moved file with new content in repository: %w", err) + } + } else { + // For simple moves without content changes, use the move operation + if err = r.repo.Move(ctx, opts.OriginalPath, opts.Path, opts.Ref, opts.Message); err != nil { + return nil, fmt.Errorf("move file in repository: %w", err) + } + } + + // Update the grafana database if this is the main branch + if opts.Ref == "" && newParsed.Client != nil { + if _, err := r.folders.EnsureFolderPathExist(ctx, opts.Path); err != nil { + return nil, fmt.Errorf("ensure folder path exists: %w", err) + } + + // Delete the old resource from grafana if name changed + if newParsed.Obj.GetName() != parsed.Obj.GetName() { + err = parsed.Client.Delete(ctx, parsed.Obj.GetName(), metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("delete original resource from storage: %w", err) + } + } + + // Create/update the new resource in grafana + err = newParsed.Run(ctx) + if err != nil { + return nil, fmt.Errorf("create moved resource in storage: %w", err) + } + } + + newParsed.Action = provisioning.ResourceActionMove + + return newParsed, nil +} + func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource, verb string) error { id, err := identity.GetRequester(ctx) if err != nil { @@ -376,6 +587,14 @@ func getFolderURLs(ctx context.Context, path, ref string, repo repository.Reposi return nil, nil } +// getPathType returns a human-readable description of the path type +func getPathType(isDir bool) string { + if isDir { + return "directory (ends with '/')" + } + return "file (no trailing '/')" +} + func folderDeleteResponse(ctx context.Context, path, ref string, repo repository.Repository) (*ParsedResource, error) { urls, err := getFolderURLs(ctx, path, ref, repo) if err != nil { diff --git a/pkg/registry/apis/provisioning/resources/dualwriter_test.go b/pkg/registry/apis/provisioning/resources/dualwriter_test.go new file mode 100644 index 00000000000..10ec8270655 --- /dev/null +++ b/pkg/registry/apis/provisioning/resources/dualwriter_test.go @@ -0,0 +1,147 @@ +package resources + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" +) + +func TestGetPathType(t *testing.T) { + tests := []struct { + name string + isDir bool + expected string + }{ + { + name: "directory path", + isDir: true, + expected: "directory (ends with '/')", + }, + { + name: "file path", + isDir: false, + expected: "file (no trailing '/')", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getPathType(tt.isDir) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMovePathValidation(t *testing.T) { + tests := []struct { + name string + originalPath string + newPath string + expectError bool + errorMessage string + }{ + { + name: "file to file move (valid)", + originalPath: "old/file.json", + newPath: "new/file.json", + expectError: false, + }, + { + name: "directory to directory move (valid)", + originalPath: "old/folder/", + newPath: "new/folder/", + expectError: false, + }, + { + name: "file to directory move (invalid)", + originalPath: "old/file.json", + newPath: "new/folder/", + expectError: true, + errorMessage: "cannot move between file and directory types", + }, + { + name: "directory to file move (invalid)", + originalPath: "old/folder/", + newPath: "new/file.json", + expectError: true, + errorMessage: "cannot move between file and directory types", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test the path validation logic that would be used in MoveResource + sourceIsDir := safepath.IsDir(tt.originalPath) + targetIsDir := safepath.IsDir(tt.newPath) + + if tt.expectError { + assert.NotEqual(t, sourceIsDir, targetIsDir, "Path types should be different for invalid moves") + } else { + assert.Equal(t, sourceIsDir, targetIsDir, "Path types should be the same for valid moves") + } + }) + } +} + +func TestMoveOptionsContentHandling(t *testing.T) { + tests := []struct { + name string + opts DualWriteOptions + originalData []byte + expectedContentToUse []byte + expectedUseOriginal bool + }{ + { + name: "move with new content provided", + opts: DualWriteOptions{ + Path: "new/file.json", + OriginalPath: "old/file.json", + Data: []byte(`{"updated": "content"}`), + }, + originalData: []byte(`{"original": "content"}`), + expectedContentToUse: []byte(`{"updated": "content"}`), + expectedUseOriginal: false, + }, + { + name: "move without new content (nil)", + opts: DualWriteOptions{ + Path: "new/file.json", + OriginalPath: "old/file.json", + Data: nil, + }, + originalData: []byte(`{"original": "content"}`), + expectedContentToUse: []byte(`{"original": "content"}`), + expectedUseOriginal: true, + }, + { + name: "move without new content (empty slice)", + opts: DualWriteOptions{ + Path: "new/file.json", + OriginalPath: "old/file.json", + Data: []byte{}, + }, + originalData: []byte(`{"original": "content"}`), + expectedContentToUse: []byte(`{"original": "content"}`), + expectedUseOriginal: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Simulate the content selection logic from moveFile method + var destinationData []byte + useOriginal := len(tt.opts.Data) == 0 + + if useOriginal { + destinationData = tt.originalData + } else { + destinationData = tt.opts.Data + } + + assert.Equal(t, tt.expectedUseOriginal, useOriginal, "Should correctly determine whether to use original content") + assert.Equal(t, tt.expectedContentToUse, destinationData, "Should select correct content for destination") + }) + } +} diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index 4f1beaa5e67..0faaab04b78 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -1281,6 +1281,14 @@ "schema": { "type": "boolean" } + }, + { + "name": "originalPath", + "in": "query", + "description": "path of file to move (used with POST method for move operations). Must be same type as target path: file-to-file (e.g., 'some/a.json' -\u003e 'c/d.json') or folder-to-folder (e.g., 'some/' -\u003e 'new/')", + "schema": { + "type": "string" + } } ], "requestBody": { @@ -1385,6 +1393,14 @@ "schema": { "type": "boolean" } + }, + { + "name": "originalPath", + "in": "query", + "description": "path of file to move (used with POST method for move operations). Must be same type as target path: file-to-file (e.g., 'some/a.json' -\u003e 'c/d.json') or folder-to-folder (e.g., 'some/' -\u003e 'new/')", + "schema": { + "type": "string" + } } ], "requestBody": { @@ -1489,6 +1505,14 @@ "schema": { "type": "boolean" } + }, + { + "name": "originalPath", + "in": "query", + "description": "path of file to move (used with POST method for move operations). Must be same type as target path: file-to-file (e.g., 'some/a.json' -\u003e 'c/d.json') or folder-to-folder (e.g., 'some/' -\u003e 'new/')", + "schema": { + "type": "string" + } } ], "responses": { @@ -3714,11 +3738,12 @@ ], "properties": { "action": { - "description": "The action required/used for dryRun\n\nPossible enum values:\n - `\"create\"`\n - `\"delete\"`\n - `\"update\"`", + "description": "The action required/used for dryRun\n\nPossible enum values:\n - `\"create\"`\n - `\"delete\"`\n - `\"move\"`\n - `\"update\"`", "type": "string", "enum": [ "create", "delete", + "move", "update" ] }, diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index d2787e68892..cff6dbeeabf 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -3,6 +3,9 @@ package provisioning import ( "context" "encoding/json" + "fmt" + "net/http" + "net/url" "os" "path" "strings" @@ -336,3 +339,44 @@ func unstructuredToRepository(t *testing.T, obj *unstructured.Unstructured) *pro return repo } + +// postFilesRequest performs a direct HTTP POST request to the files API. +// This bypasses Kubernetes REST client limitations with '/' characters in subresource names. +type filesPostOptions struct { + targetPath string // The target file/directory path + originalPath string // Source path for move operations (optional) + message string // Commit message (optional) + body string // Request body content (optional) + ref string // Git ref/branch (optional) +} + +func (h *provisioningTestHelper) postFilesRequest(t *testing.T, repo string, opts filesPostOptions) *http.Response { + addr := h.GetEnv().Server.HTTPServer.Listener.Addr().String() + baseUrl := fmt.Sprintf("http://admin:admin@%s/apis/provisioning.grafana.app/v0alpha1/namespaces/default/repositories/%s/files/%s", + addr, repo, opts.targetPath) + + // Build the URL with proper query parameter encoding + parsedUrl, err := url.Parse(baseUrl) + require.NoError(t, err) + params := parsedUrl.Query() + + if opts.originalPath != "" { + params.Set("originalPath", opts.originalPath) + } + if opts.message != "" { + params.Set("message", opts.message) + } + if opts.ref != "" { + params.Set("ref", opts.ref) + } + parsedUrl.RawQuery = params.Encode() + + req, err := http.NewRequest(http.MethodPost, parsedUrl.String(), strings.NewReader(opts.body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + return resp +} diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index 7fa26a30fb6..a5edc751fbe 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "io/fs" "net/http" "os" @@ -820,3 +821,286 @@ func TestIntegrationProvisioning_DeleteResources(t *testing.T) { require.Error(t, result.Error()) }) } + +func TestIntegrationProvisioning_MoveResources(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + helper := runGrafana(t) + ctx := context.Background() + + const repo = "move-test-repo" + localTmp := helper.RenderObject(t, "testdata/local-write.json.tmpl", map[string]any{ + "Name": repo, + "SyncEnabled": true, + "SyncTarget": "instance", + }) + _, err := helper.Repositories.Resource.Create(ctx, localTmp, metav1.CreateOptions{}) + require.NoError(t, err) + + // Copy test dashboards to the repository path for initial setup + const originalDashboard = "all-panels.json" + helper.CopyToProvisioningPath(t, "testdata/all-panels.json", originalDashboard) + + // Wait for sync to ensure the dashboard is created in Grafana + helper.SyncAndWait(t, repo, nil) + + // Verify the original dashboard exists in Grafana (using the UID from all-panels.json) + const allPanelsUID = "n1jR8vnnz" // This is the UID from the all-panels.json file + obj, err := helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{}) + require.NoError(t, err, "original dashboard should exist in Grafana") + require.Equal(t, repo, obj.GetAnnotations()[utils.AnnoKeyManagerIdentity]) + + t.Run("move file without content change", func(t *testing.T) { + const targetPath = "moved/simple-move.json" + + // Perform the move operation using helper function + resp := helper.postFilesRequest(t, repo, filesPostOptions{ + targetPath: targetPath, + originalPath: originalDashboard, + message: "move file without content change", + }) + // nolint:errcheck + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode, "move operation should succeed") + + // Verify the file moved in the repository + movedObj, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved", "simple-move.json") + require.NoError(t, err, "moved file should exist in repository") + + // Check the content is preserved (verify it's still the all-panels dashboard) + resource, _, err := unstructured.NestedMap(movedObj.Object, "resource") + require.NoError(t, err) + dryRun, _, err := unstructured.NestedMap(resource, "dryRun") + require.NoError(t, err) + title, _, err := unstructured.NestedString(dryRun, "spec", "title") + require.NoError(t, err) + require.Equal(t, "Panel tests - All panels", title, "content should be preserved") + + // Verify original file no longer exists + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", originalDashboard) + require.Error(t, err, "original file should no longer exist") + + // Verify dashboard still exists in Grafana with same content but may have updated path references + helper.SyncAndWait(t, repo, nil) + _, err = helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{}) + require.NoError(t, err, "dashboard should still exist in Grafana after move") + }) + + t.Run("move file to nested path without ref", func(t *testing.T) { + // Test a different scenario: Move a file that was never synced to Grafana + // This might reveal the issue if dashboard creation fails during move + const sourceFile = "never-synced.json" + helper.CopyToProvisioningPath(t, "testdata/timeline-demo.json", sourceFile) + + // DO NOT sync - move the file immediately without it ever being in Grafana + const targetPath = "deep/nested/timeline.json" + + // Perform the move operation without the file ever being synced to Grafana + resp := helper.postFilesRequest(t, repo, filesPostOptions{ + targetPath: targetPath, + originalPath: sourceFile, + message: "move never-synced file to nested path", + }) + // nolint:errcheck + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode, "move operation should succeed") + + // Check folders were created and validate hierarchy + folderList, err := helper.Folders.Resource.List(ctx, metav1.ListOptions{}) + require.NoError(t, err, "should be able to list folders") + + // Build a map of folder names to their objects for easier lookup + folders := make(map[string]*unstructured.Unstructured) + for _, folder := range folderList.Items { + title, _, _ := unstructured.NestedString(folder.Object, "spec", "title") + folders[title] = &folder + parent, _, _ := unstructured.NestedString(folder.Object, "metadata", "annotations", "grafana.app/folder") + t.Logf(" - %s: %s (parent: %s)", folder.GetName(), title, parent) + } + + // Validate expected folders exist with proper hierarchy + // Expected structure: deep -> deep/nested + deepFolderTitle := "deep" + nestedFolderTitle := "nested" + + // Validate "deep" folder exists and has no parent (is top-level) + require.Contains(t, folders, deepFolderTitle, "deep folder should exist") + f := folders[deepFolderTitle] + deepFolderName := f.GetName() + title, _, _ := unstructured.NestedString(f.Object, "spec", "title") + require.Equal(t, deepFolderTitle, title, "deep folder should have correct title") + parent, found, _ := unstructured.NestedString(f.Object, "metadata", "annotations", "grafana.app/folder") + require.True(t, !found || parent == "", "deep folder should be top-level (no parent)") + + // Validate "deep/nested" folder exists and has "deep" as parent + require.Contains(t, folders, nestedFolderTitle, "nested folder should exist") + f = folders[nestedFolderTitle] + nestedFolderName := f.GetName() + title, _, _ = unstructured.NestedString(f.Object, "spec", "title") + require.Equal(t, nestedFolderTitle, title, "nested folder should have correct title") + parent, _, _ = unstructured.NestedString(f.Object, "metadata", "annotations", "grafana.app/folder") + require.Equal(t, deepFolderName, parent, "nested folder should have deep folder as parent") + + // The key test: Check if dashboard was created in Grafana during move + const timelineUID = "mIJjFy8Kz" + dashboard, err := helper.DashboardsV1.Resource.Get(ctx, timelineUID, metav1.GetOptions{}) + require.NoError(t, err, "dashboard should exist in Grafana after moving never-synced file") + dashboardFolder, _, _ := unstructured.NestedString(dashboard.Object, "metadata", "annotations", "grafana.app/folder") + + // Validate dashboard is in the correct nested folder + require.Equal(t, nestedFolderName, dashboardFolder, "dashboard should be in the nested folder") + + // Verify the file moved in the repository + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "deep", "nested", "timeline.json") + require.NoError(t, err, "moved file should exist in nested repository path") + + // Verify the original file no longer exists in the repository + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", sourceFile) + require.Error(t, err, "original file should no longer exist in repository") + }) + + t.Run("move file with content update", func(t *testing.T) { + const sourcePath = "moved/simple-move.json" // Use the file from previous test + const targetPath = "updated/content-updated.json" + + // Use text-options.json content for the update + updatedContent := helper.LoadFile("testdata/text-options.json") + + // Perform move with content update using helper function + resp := helper.postFilesRequest(t, repo, filesPostOptions{ + targetPath: targetPath, + originalPath: sourcePath, + message: "move file with content update", + body: string(updatedContent), + }) + // nolint:errcheck + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode, "move with content update should succeed") + + // Verify the moved file has updated content (should now be text-options dashboard) + movedObj, err := helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "updated", "content-updated.json") + require.NoError(t, err, "moved file should exist in repository") + + resource, _, err := unstructured.NestedMap(movedObj.Object, "resource") + require.NoError(t, err) + dryRun, _, err := unstructured.NestedMap(resource, "dryRun") + require.NoError(t, err) + title, _, err := unstructured.NestedString(dryRun, "spec", "title") + require.NoError(t, err) + require.Equal(t, "Text options", title, "content should be updated to text-options dashboard") + + // Check it has the expected UID from text-options.json + name, _, err := unstructured.NestedString(dryRun, "metadata", "name") + require.NoError(t, err) + require.Equal(t, "WZ7AhQiVz", name, "should have the UID from text-options.json") + + // Verify source file no longer exists + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved", "simple-move.json") + require.Error(t, err, "source file should no longer exist") + + // Sync and verify the updated dashboard exists in Grafana + helper.SyncAndWait(t, repo, nil) + const textOptionsUID = "WZ7AhQiVz" // UID from text-options.json + updatedDashboard, err := helper.DashboardsV1.Resource.Get(ctx, textOptionsUID, metav1.GetOptions{}) + require.NoError(t, err, "updated dashboard should exist in Grafana") + + // Verify the original dashboard was deleted from Grafana + _, err = helper.DashboardsV1.Resource.Get(ctx, allPanelsUID, metav1.GetOptions{}) + require.Error(t, err, "original dashboard should be deleted from Grafana") + require.True(t, apierrors.IsNotFound(err)) + + // Verify the new dashboard has the updated content + updatedTitle, _, err := unstructured.NestedString(updatedDashboard.Object, "spec", "title") + require.NoError(t, err) + require.Equal(t, "Text options", updatedTitle) + }) + + t.Run("move directory", func(t *testing.T) { + // Create some files in a directory first using existing testdata files + helper.CopyToProvisioningPath(t, "testdata/timeline-demo.json", "source-dir/timeline-demo.json") + helper.CopyToProvisioningPath(t, "testdata/text-options.json", "source-dir/text-options.json") + + // Sync to ensure files are recognized + helper.SyncAndWait(t, repo, nil) + + const sourceDir = "source-dir/" + const targetDir = "moved-dir/" + + // Move directory using helper function + resp := helper.postFilesRequest(t, repo, filesPostOptions{ + targetPath: targetDir, + originalPath: sourceDir, + message: "move directory", + }) + // nolint:errcheck + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode, "directory move should succeed") + + // Verify source directory no longer exists + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "source-dir") + require.Error(t, err, "source directory should no longer exist") + + // Verify target directory and files exist + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved-dir", "timeline-demo.json") + require.NoError(t, err, "moved timeline-demo.json should exist") + _, err = helper.Repositories.Resource.Get(ctx, repo, metav1.GetOptions{}, "files", "moved-dir", "text-options.json") + require.NoError(t, err, "moved text-options.json should exist") + }) + + t.Run("error cases", func(t *testing.T) { + t.Run("missing originalPath parameter", func(t *testing.T) { + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "target.json"). + Body([]byte(`{"test": "content"}`)). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.Error(t, result.Error(), "should fail without originalPath") + }) + + t.Run("file to directory type mismatch", func(t *testing.T) { + // First create a simple test file without slashes in the path + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "simple-test.json"). + Body(helper.LoadFile("testdata/all-panels.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.NoError(t, result.Error(), "should create test file") + + // Now try to move this file to a directory path using helper function + resp := helper.postFilesRequest(t, repo, filesPostOptions{ + targetPath: "target-dir/", + originalPath: "simple-test.json", + message: "test move", + }) + // nolint:errcheck + defer resp.Body.Close() + // Read response body to check error message + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + require.NotEqual(t, http.StatusOK, resp.StatusCode, "should fail when moving file to directory") + require.Contains(t, string(body), "cannot move between file and directory types") + }) + + t.Run("non-existent source file", func(t *testing.T) { + result := helper.AdminREST.Post(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", "target.json"). + Param("originalPath", "non-existent.json"). + Body([]byte("")). + SetHeader("Content-Type", "application/json"). + Do(ctx) + require.Error(t, result.Error(), "should fail when source file doesn't exist") + }) + }) +} diff --git a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts index 0ecc569f2cb..ecd79c6c52e 100644 --- a/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts +++ b/public/app/api/clients/provisioning/v0alpha1/endpoints.gen.ts @@ -161,6 +161,7 @@ const injectedRtkApi = api ref: queryArg.ref, message: queryArg.message, skipDryRun: queryArg.skipDryRun, + originalPath: queryArg.originalPath, }, }), invalidatesTags: ['Repository'], @@ -177,6 +178,7 @@ const injectedRtkApi = api ref: queryArg.ref, message: queryArg.message, skipDryRun: queryArg.skipDryRun, + originalPath: queryArg.originalPath, }, }), invalidatesTags: ['Repository'], @@ -192,6 +194,7 @@ const injectedRtkApi = api ref: queryArg.ref, message: queryArg.message, skipDryRun: queryArg.skipDryRun, + originalPath: queryArg.originalPath, }, }), invalidatesTags: ['Repository'], @@ -527,6 +530,8 @@ export type ReplaceRepositoryFilesWithPathApiArg = { message?: string; /** do not pro-actively verify the payload */ skipDryRun?: boolean; + /** path of file to move (used with POST method for move operations). Must be same type as target path: file-to-file (e.g., 'some/a.json' -> 'c/d.json') or folder-to-folder (e.g., 'some/' -> 'new/') */ + originalPath?: string; body: { [key: string]: any; }; @@ -543,6 +548,8 @@ export type CreateRepositoryFilesWithPathApiArg = { message?: string; /** do not pro-actively verify the payload */ skipDryRun?: boolean; + /** path of file to move (used with POST method for move operations). Must be same type as target path: file-to-file (e.g., 'some/a.json' -> 'c/d.json') or folder-to-folder (e.g., 'some/' -> 'new/') */ + originalPath?: string; body: { [key: string]: any; }; @@ -559,6 +566,8 @@ export type DeleteRepositoryFilesWithPathApiArg = { message?: string; /** do not pro-actively verify the payload */ skipDryRun?: boolean; + /** path of file to move (used with POST method for move operations). Must be same type as target path: file-to-file (e.g., 'some/a.json' -> 'c/d.json') or folder-to-folder (e.g., 'some/' -> 'new/') */ + originalPath?: string; }; export type GetRepositoryHistoryApiResponse = /** status 200 OK */ string; export type GetRepositoryHistoryApiArg = { @@ -1121,8 +1130,9 @@ export type ResourceObjects = { Possible enum values: - `"create"` - `"delete"` + - `"move"` - `"update"` */ - action?: 'create' | 'delete' | 'update'; + action?: 'create' | 'delete' | 'move' | 'update'; /** The value returned from a dryRun request */ dryRun?: Unstructured; /** The same value, currently saved in the grafana database */ From 761150fcb8dc96532657ef2ffd485a2185bc9532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Sencer=20=C3=96zcan?= <32759850+mustafasencer@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:46:02 +0200 Subject: [PATCH 086/101] fix: enable dashboard api k8s list tests (#106508) * fix: separate dashboard api tests * fix: typo * refactor: revert file rename * fix: compilable * fix: trial * fix: remove unused func * Pass the right value for kubernetesDashboardsEnabled flag Signed-off-by: Maicon Costa * Enable mode 3 for remaining tests Signed-off-by: Maicon Costa * Disable integration tests for MySQL and Postgres Signed-off-by: Maicon Costa * Run go work use Signed-off-by: Maicon Costa * Update go.mod Signed-off-by: Maicon Costa * Remove TODO comment Signed-off-by: Maicon Costa * Revert to go 1.24.5 Signed-off-by: Maicon Costa * go.mod Signed-off-by: Maicon Costa * Remove duplicated block Signed-off-by: Maicon Costa * Experiment without reusing client Signed-off-by: Maicon Costa * Reenable integration tests on Postgres and MySQL Signed-off-by: Maicon Costa * Revert "Experiment without reusing client" This reverts commit 0126e321a08dbafc482a623f2593edaf5561ed32. * Reapply "Experiment without reusing client" This reverts commit 5368b4c5318f78aef9b1a5c310846f3f5569fe78. * Refactor how we get an adminClient Signed-off-by: Maicon Costa * Add TODO comments Signed-off-by: Maicon Costa --------- Signed-off-by: Maicon Costa Co-authored-by: Maicon Costa --- .../integration/api_validation_test.go | 429 ++++++++++-------- 1 file changed, 247 insertions(+), 182 deletions(-) diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index c7242b31340..881ea3c1430 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -56,8 +56,8 @@ type TestContext struct { OrgID int64 } -// TestIntegrationValidation tests the dashboard K8s API -func TestIntegrationValidation(t *testing.T) { +// TestIntegrationDashboardAPIValidation tests the dashboard K8s API with validation checks +func TestIntegrationDashboardAPIValidation(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } @@ -79,7 +79,19 @@ func TestIntegrationValidation(t *testing.T) { }, }}) - testIntegrationValidationForServer(t, helper, dualWriterMode) + t.Cleanup(func() { + helper.Shutdown() + }) + + org1Ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) + + t.Run("Dashboard validation tests", func(t *testing.T) { + runDashboardValidationTests(t, org1Ctx) + }) + + t.Run("Dashboard quota tests", func(t *testing.T) { + runQuotaTests(t, org1Ctx) + }) }) } @@ -114,45 +126,110 @@ func TestIntegrationValidation(t *testing.T) { } } -func testIntegrationValidationForServer(t *testing.T, helper *apis.K8sTestHelper, dualWriterMode rest.DualWriterMode) { - t.Cleanup(func() { - helper.Shutdown() - }) +// TestIntegrationDashboardAPIAuthorization tests the dashboard K8s API with authorization checks +func TestIntegrationDashboardAPIAuthorization(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } - // Create test contexts organization - org1Ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) - org2Ctx := createTestContext(t, helper, helper.OrgB, dualWriterMode) + dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, dualWriterMode := range dualWriterModes { + t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature + featuremgmt.FlagUnifiedStorageSearch, + }, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: dualWriterMode, + }, + "folders.folder.grafana.app": { + DualWriterMode: dualWriterMode, + }, + }}) - t.Run("Organization 1 tests", func(t *testing.T) { - t.Run("Dashboard validation tests", func(t *testing.T) { - runDashboardValidationTests(t, org1Ctx) + t.Cleanup(func() { + helper.Shutdown() + }) + + org1Ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) + org2Ctx := createTestContext(t, helper, helper.OrgB, dualWriterMode) + + t.Run("Authorization tests for all identity types", func(t *testing.T) { + runAuthorizationTests(t, org1Ctx) + }) + + t.Run("Dashboard permission tests", func(t *testing.T) { + runDashboardPermissionTests(t, org1Ctx, false) + }) + + t.Run("Cross-organization tests", func(t *testing.T) { + runCrossOrgTests(t, org1Ctx, org2Ctx) + }) + + t.Run("Dashboard HTTP API test", func(t *testing.T) { + runDashboardHttpTest(t, org1Ctx, org2Ctx) + }) }) + } +} - t.Run("Dashboard quota tests", func(t *testing.T) { - runQuotaTests(t, org1Ctx) +// TestIntegrationDashboardAPI tests the dashboard K8s API +func TestIntegrationDashboardAPI(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + dualWriterModes := []rest.DualWriterMode{rest.Mode0, rest.Mode1, rest.Mode2, rest.Mode3, rest.Mode4, rest.Mode5} + for _, dualWriterMode := range dualWriterModes { + t.Run(fmt.Sprintf("DualWriterMode %d", dualWriterMode), func(t *testing.T) { + // Create a K8sTestHelper which will set up a real API server + helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableFeatureToggles: []string{ + featuremgmt.FlagKubernetesClientDashboardsFolders, // Enable dashboard feature + featuremgmt.FlagUnifiedStorageSearch, + featuremgmt.FlagKubernetesDashboards, + }, + UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ + "dashboards.dashboard.grafana.app": { + DualWriterMode: dualWriterMode, + }, + "folders.folder.grafana.app": { + DualWriterMode: dualWriterMode, + }, + }}) + + t.Cleanup(func() { + helper.Shutdown() + }) + + org1Ctx := createTestContext(t, helper, helper.Org1, dualWriterMode) + org2Ctx := createTestContext(t, helper, helper.OrgB, dualWriterMode) + + t.Run("Dashboard LIST API test", func(t *testing.T) { + runDashboardListTests(t, org1Ctx) + }) + + t.Run("Authorization tests for all identity types", func(t *testing.T) { + runAuthorizationTests(t, org1Ctx) + }) + + t.Run("Dashboard permission tests", func(t *testing.T) { + runDashboardPermissionTests(t, org1Ctx, true) + }) + + t.Run("Dashboard HTTP API test", func(t *testing.T) { + runDashboardHttpTest(t, org1Ctx, org2Ctx) + }) + + t.Run("Cross-organization tests", func(t *testing.T) { + runCrossOrgTests(t, org1Ctx, org2Ctx) + }) }) - - t.Run("Authorization tests for all identity types", func(t *testing.T) { - runAuthorizationTests(t, org1Ctx) - }) - - t.Run("Dashboard permission tests", func(t *testing.T) { - runDashboardPermissionTests(t, org1Ctx, true) - }) - - t.Run("Dashboard LIST API test", func(t *testing.T) { - t.Skip("Skip LIST") - runDashboardListTest(t, org1Ctx) - }) - }) - - t.Run("Dashboard HTTP API test", func(t *testing.T) { - runDashboardHttpTest(t, org1Ctx, org2Ctx) - }) - - t.Run("Cross-organization tests", func(t *testing.T) { - runCrossOrgTests(t, org1Ctx, org2Ctx) - }) + } } // Auth identity types (user or token) with resource client @@ -169,7 +246,11 @@ type Identity struct { func runDashboardValidationTests(t *testing.T, ctx TestContext) { t.Helper() - adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR()) + // Get a new resource client for admin user + // TODO: we need to figure out why reusing the same client results in slower tests + adminClient := func() *apis.K8sResourceClient { + return getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR()) + } editorClient := getResourceClient(t, ctx.Helper, ctx.EditorUser, getDashboardGVR()) t.Run("Dashboard UID validations", func(t *testing.T) { @@ -177,15 +258,15 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { t.Run("reject dashboard with existing UID", func(t *testing.T) { // Create a dashboard with a specific UID specificUID := "existing-uid-dash" - createdDash, err := createDashboard(t, adminClient, "Dashboard with Specific UID", nil, &specificUID) + createdDash, err := createDashboard(t, adminClient(), "Dashboard with Specific UID", nil, &specificUID) require.NoError(t, err) // Try to create another dashboard with the same UID - _, err = createDashboard(t, adminClient, "Another Dashboard with Same UID", nil, &specificUID) + _, err = createDashboard(t, adminClient(), "Another Dashboard with Same UID", nil, &specificUID) require.Error(t, err) // Clean up - err = adminClient.Resource.Delete(context.Background(), createdDash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), createdDash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) }) @@ -193,14 +274,14 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { t.Run("reject dashboard with too long UID", func(t *testing.T) { // Create a dashboard with a long UID (over 40 chars) longUID := "this-uid-is-way-too-long-for-a-dashboard-uid-12345678901234567890" - _, err := createDashboard(t, adminClient, "Dashboard with Long UID", nil, &longUID) + _, err := createDashboard(t, adminClient(), "Dashboard with Long UID", nil, &longUID) require.Error(t, err) }) // Test creating dashboard with invalid UID characters t.Run("reject dashboard with invalid UID characters", func(t *testing.T) { invalidUID := "invalid/uid/with/slashes" - _, err := createDashboard(t, adminClient, "Dashboard with Invalid UID", nil, &invalidUID) + _, err := createDashboard(t, adminClient(), "Dashboard with Invalid UID", nil, &invalidUID) require.Error(t, err) }) }) @@ -209,47 +290,47 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { t.Run("Dashboard title validations", func(t *testing.T) { // Test empty title t.Run("reject dashboard with empty title", func(t *testing.T) { - _, err := createDashboard(t, adminClient, "", nil, nil) + _, err := createDashboard(t, adminClient(), "", nil, nil) require.Error(t, err) }) // Test long title t.Run("reject dashboard with excessively long title", func(t *testing.T) { veryLongTitle := strings.Repeat("a", 10000) - _, err := createDashboard(t, adminClient, veryLongTitle, nil, nil) + _, err := createDashboard(t, adminClient(), veryLongTitle, nil, nil) require.Error(t, err) }) // Test updating dashboard with empty title t.Run("reject dashboard update with empty title", func(t *testing.T) { // First create a valid dashboard - dash, err := createDashboard(t, adminClient, "Valid Dashboard Title", nil, nil) + dash, err := createDashboard(t, adminClient(), "Valid Dashboard Title", nil, nil) require.NoError(t, err) require.NotNil(t, dash) // Try to update with empty title - _, err = updateDashboard(t, adminClient, dash, "", nil) + _, err = updateDashboard(t, adminClient(), dash, "", nil) require.Error(t, err) // Clean up - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) }) // Test updating dashboard with excessively long title t.Run("reject dashboard update with excessively long title", func(t *testing.T) { // First create a valid dashboard - dash, err := createDashboard(t, adminClient, "Valid Dashboard Title", nil, nil) + dash, err := createDashboard(t, adminClient(), "Valid Dashboard Title", nil, nil) require.NoError(t, err) require.NotNil(t, dash) // Try to update with excessively long title veryLongTitle := strings.Repeat("a", 10000) - _, err = updateDashboard(t, adminClient, dash, veryLongTitle, nil) + _, err = updateDashboard(t, adminClient(), dash, veryLongTitle, nil) require.Error(t, err) // Clean up - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) }) }) @@ -257,15 +338,15 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { t.Run("Dashboard message validations", func(t *testing.T) { // Test long message t.Run("reject dashboard with excessively long update message", func(t *testing.T) { - dash, err := createDashboard(t, adminClient, "Regular dashboard", nil, nil) + dash, err := createDashboard(t, adminClient(), "Regular dashboard", nil, nil) require.NoError(t, err) veryLongMessage := strings.Repeat("a", 600) - _, err = updateDashboard(t, adminClient, dash, "Dashboard updated with a long message", &veryLongMessage) + _, err = updateDashboard(t, adminClient(), dash, "Dashboard updated with a long message", &veryLongMessage) require.Error(t, err) // Clean up - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) }) }) @@ -274,21 +355,21 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { // Test non-existent folder UID t.Run("reject dashboard with non-existent folder UID", func(t *testing.T) { nonExistentFolderUID := "non-existent-folder-uid" - _, err := createDashboard(t, adminClient, "Dashboard in Non-existent Folder", &nonExistentFolderUID, nil) + _, err := createDashboard(t, adminClient(), "Dashboard in Non-existent Folder", &nonExistentFolderUID, nil) ctx.Helper.EnsureStatusError(err, http.StatusNotFound, "folders.folder.grafana.app \"non-existent-folder-uid\" not found") }) t.Run("allow moving folder to general folder", func(t *testing.T) { folder1 := createFolderObject(t, "folder1", "default", "") folder1UID := folder1.GetName() - dash, err := createDashboard(t, adminClient, "Dashboard in a Folder", &folder1UID, nil) + dash, err := createDashboard(t, adminClient(), "Dashboard in a Folder", &folder1UID, nil) require.NoError(t, err) generalFolderUID := "" - _, err = updateDashboard(t, adminClient, dash, "Move dashboard into the General Folder", &generalFolderUID) + _, err = updateDashboard(t, adminClient(), dash, "Move dashboard into the General Folder", &generalFolderUID) require.NoError(t, err) - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) }) }) @@ -403,7 +484,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { // Test version increment on update t.Run("version increments on dashboard update", func(t *testing.T) { // Create a dashboard with admin - dash, err := createDashboard(t, adminClient, "Dashboard for Version Test", nil, nil) + dash, err := createDashboard(t, adminClient(), "Dashboard for Version Test", nil, nil) require.NoError(t, err, "Failed to create dashboard for version test") dashUID := dash.GetName() @@ -413,7 +494,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { initialRV := meta.GetResourceVersion() // Update the dashboard - updatedDash, err := updateDashboard(t, adminClient, dash, "Updated Dashboard for Version Test", nil) + updatedDash, err := updateDashboard(t, adminClient(), dash, "Updated Dashboard for Version Test", nil) require.NoError(t, err) require.NotNil(t, updatedDash) @@ -423,25 +504,25 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { require.NotEqual(t, meta.GetResourceVersion(), initialRV, "Resource version should be changed after update") // Clean up - err = adminClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{}) require.NoError(t, err) }) // Test generation conflict when updating concurrently t.Run("reject update with version conflict", func(t *testing.T) { // Create a dashboard with admin - dash, err := createDashboard(t, adminClient, "Dashboard for Version Conflict Test", nil, nil) + dash, err := createDashboard(t, adminClient(), "Dashboard for Version Conflict Test", nil, nil) require.NoError(t, err, "Failed to create dashboard for version conflict test") dashUID := dash.GetName() // Get the dashboard twice (simulating two users getting it) - dash1, err := adminClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{}) + dash1, err := adminClient().Resource.Get(context.Background(), dashUID, v1.GetOptions{}) require.NoError(t, err) dash2, err := editorClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{}) require.NoError(t, err) // Update with the first copy - updatedDash1, err := updateDashboard(t, adminClient, dash1, "Updated by first user", nil) + updatedDash1, err := updateDashboard(t, adminClient(), dash1, "Updated by first user", nil) require.NoError(t, err) require.NotNil(t, updatedDash1) @@ -451,7 +532,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { require.Contains(t, err.Error(), "the object has been modified", "Should fail with version conflict error") // Clean up - err = adminClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{}) require.NoError(t, err) }) @@ -464,12 +545,12 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { meta.SetGeneration(5) // Create the dashboard - createdDash, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{}) + createdDash, err := adminClient().Resource.Create(context.Background(), dashObj, v1.CreateOptions{}) require.NoError(t, err) dashUID := createdDash.GetName() // Fetch the created dashboard - fetchedDash, err := adminClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{}) + fetchedDash, err := adminClient().Resource.Get(context.Background(), dashUID, v1.GetOptions{}) require.NoError(t, err) // Verify the generation was handled properly @@ -477,22 +558,22 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { require.Equal(t, 5, meta.GetGeneration(), "Generation should be 5") // Clean up - err = adminClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{}) require.NoError(t, err) }) t.Run("dashboard version history available, even for UIDs ending in hyphen", func(t *testing.T) { dashboardUID := "test-dashboard-" - dash, err := createDashboard(t, adminClient, "Dashboard with uid ending in hyphen", nil, &dashboardUID) + dash, err := createDashboard(t, adminClient(), "Dashboard with uid ending in hyphen", nil, &dashboardUID) require.NoError(t, err) - updatedDash, err := updateDashboard(t, adminClient, dash, "Updated dashboard with uid ending in hyphen", nil) + updatedDash, err := updateDashboard(t, adminClient(), dash, "Updated dashboard with uid ending in hyphen", nil) require.NoError(t, err) require.NotNil(t, updatedDash) labelSelector := utils.LabelKeyGetHistory + "=true" fieldSelector := "metadata.name=" + dashboardUID - versions, err := adminClient.Resource.List(context.Background(), v1.ListOptions{ + versions, err := adminClient().Resource.List(context.Background(), v1.ListOptions{ LabelSelector: labelSelector, FieldSelector: fieldSelector, Limit: 10, @@ -502,7 +583,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { // one from initial save, one from update require.Equal(t, len(versions.Items), 2) - err = adminClient.Resource.Delete(context.Background(), dashboardUID, v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dashboardUID, v1.DeleteOptions{}) require.NoError(t, err) }) }) @@ -530,12 +611,12 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Create a dashboard with admin - dash, err := createDashboard(t, adminClient, "Dashboard for Provisioning Test", nil, nil) + dash, err := createDashboard(t, adminClient(), "Dashboard for Provisioning Test", nil, nil) require.NoError(t, err, "Failed to create dashboard for provisioning test") dashUID := dash.GetName() // Fetch the created dashboard - fetchedDash, err := adminClient.Resource.Get(context.Background(), dashUID, v1.GetOptions{}) + fetchedDash, err := adminClient().Resource.Get(context.Background(), dashUID, v1.GetOptions{}) require.NoError(t, err) require.NotNil(t, fetchedDash) @@ -543,7 +624,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { provisionedDash := markDashboardObjectAsProvisioned(t, fetchedDash, "test-provider", "test-external-id", "test-checksum", tc.allowsEdits) // Update the dashboard to apply the provisioning annotations - updatedDash, err := adminClient.Resource.Update(context.Background(), provisionedDash, v1.UpdateOptions{}) + updatedDash, err := adminClient().Resource.Update(context.Background(), provisionedDash, v1.UpdateOptions{}) require.NoError(t, err) require.NotNil(t, updatedDash) @@ -570,16 +651,13 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { } // Clean up - err = adminClient.Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dashUID, v1.DeleteOptions{}) require.NoError(t, err) }) } }) t.Run("Dashboard refresh interval validations", func(t *testing.T) { - // Create test client - adminClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR()) - // Store original settings to restore after test origCfg := ctx.Helper.GetEnv().Cfg origMinRefreshInterval := origCfg.MinRefreshInterval @@ -639,14 +717,14 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { _ = meta.SetSpec(specMap) - dash, err := adminClient.Resource.Create(context.Background(), dashObj, v1.CreateOptions{}) + dash, err := adminClient().Resource.Create(context.Background(), dashObj, v1.CreateOptions{}) if tc.shouldSucceed { require.NoError(t, err) require.NotNil(t, dash) // Clean up - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) } else { require.Error(t, err) @@ -664,7 +742,7 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { // Create a dashboard with a specific UID to make it easier to manage specificUID := "size-limit-test-dash" - dash, err := createDashboard(t, adminClient, "Dashboard Exceeding Size Limit", nil, &specificUID) + dash, err := createDashboard(t, adminClient(), "Dashboard Exceeding Size Limit", nil, &specificUID) require.NoError(t, err) meta, _ := utils.MetaAccessor(dash) @@ -704,44 +782,17 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { require.NoError(t, err, "Failed to set spec") // Try to update with too many panels - _, err = adminClient.Resource.Update(context.Background(), dash, v1.UpdateOptions{}) + _, err = adminClient().Resource.Update(context.Background(), dash, v1.UpdateOptions{}) require.Error(t, err) require.Contains(t, err.Error(), "exceeds", "Error should mention size or limit exceeded") // Clean up - err = adminClient.Resource.Delete(context.Background(), specificUID, v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), specificUID, v1.DeleteOptions{}) require.NoError(t, err) }) }) } -// skipIfMode skips the current test if running in any of the specified modes -// Usage: skipIfMode(t, rest.Mode1, rest.Mode4) -// or with a message: skipIfMode(t, "Known issue with conflict detection", rest.Mode1, rest.Mode4) -// nolint:unused -func (c *TestContext) skipIfMode(t *testing.T, args ...interface{}) { - t.Helper() - - message := "Test not supported in this dual writer mode" - modes := []rest.DualWriterMode{} - - // Parse args - first string is considered a message, all rest.DualWriterMode values are modes to skip - for _, arg := range args { - if msg, ok := arg.(string); ok { - message = msg - } else if mode, ok := arg.(rest.DualWriterMode); ok { - modes = append(modes, mode) - } - } - - // Check if current mode is in the list of modes to skip - for _, mode := range modes { - if c.DualWriterMode == mode { - t.Skipf("%s (mode %d)", message, c.DualWriterMode) - } - } -} - // Run tests for quota validation func runQuotaTests(t *testing.T, ctx TestContext) { t.Helper() @@ -1090,6 +1141,13 @@ func updateDashboard(t *testing.T, client *apis.K8sResourceClient, dashboard *un func runAuthorizationTests(t *testing.T, ctx TestContext) { t.Helper() + // Get a new resource client for admin user + // TODO: we need to figure out why reusing the same client results in slower tests + adminClient := func() *apis.K8sResourceClient { + // admin token + return getServiceAccountResourceClient(t, ctx.Helper, ctx.AdminServiceAccountToken, ctx.OrgID, getDashboardGVR()) + } + // Get clients for each identity type and role adminUserClient := getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR()) editorUserClient := getResourceClient(t, ctx.Helper, ctx.EditorUser, getDashboardGVR()) @@ -1112,11 +1170,13 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { {Name: "Viewer token", DashboardClient: viewerTokenClient, Type: "token"}, } + // TODO: re-enable admin cleanup clients when we have figured out why reusing the same client results in slower tests + // TODO: This is currently disabled to avoid issues with reusing the same client in tests. // Get admin clients for cleanup based on identity type - adminCleanupClients := map[string]*apis.K8sResourceClient{ - "user": adminUserClient, - "token": adminTokenClient, - } + // adminCleanupClients := map[string]*apis.K8sResourceClient{ + // "user": adminUserClient, + // "token": adminTokenClient, + // } // Define test cases for different roles type roleTest struct { @@ -1162,8 +1222,9 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { for _, identity := range identities { identity := identity // Capture range variable t.Run(identity.Name, func(t *testing.T) { + // TODO: This is currently disabled to avoid issues with reusing the same client in tests. // Get admin client for cleanup based on identity type - adminClient := adminCleanupClients[identity.Type] + // adminClient := adminCleanupClients[identity.Type] // Get role capabilities for this identity roleCapabilities := authTests[identity.DashboardClient] @@ -1195,7 +1256,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { } // Clean up - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) } else { // Test cannot create dashboard @@ -1209,7 +1270,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { // Test dashboard updates t.Run("dashboard update", func(t *testing.T) { // Create a dashboard with admin - dash, err := createDashboard(t, adminClient, "Dashboard to Update by "+identity.Name, nil, nil) + dash, err := createDashboard(t, adminClient(), "Dashboard to Update by "+identity.Name, nil, nil) require.NoError(t, err) require.NotNil(t, dash) @@ -1229,14 +1290,14 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { } // Clean up - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) }) // Test dashboard deletion permissions t.Run("dashboard deletion", func(t *testing.T) { // Create a dashboard with admin - dash, err := createDashboard(t, adminClient, "Dashboard for deletion test by "+identity.Name, nil, nil) + dash, err := createDashboard(t, adminClient(), "Dashboard for deletion test by "+identity.Name, nil, nil) require.NoError(t, err) require.NotNil(t, dash) @@ -1247,7 +1308,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { } else { require.Error(t, err, "Should not be able to delete dashboard") // Clean up with admin if the test identity couldn't delete - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) } }) @@ -1256,7 +1317,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { // Test dashboard viewing for all roles t.Run("dashboard viewing", func(t *testing.T) { // Create a dashboard with admin - dash, err := createDashboard(t, adminClient, "Dashboard for "+identity.Name+" to view", nil, nil) + dash, err := createDashboard(t, adminClient(), "Dashboard for "+identity.Name+" to view", nil, nil) require.NoError(t, err) require.NotNil(t, dash) @@ -1266,7 +1327,7 @@ func runAuthorizationTests(t *testing.T, ctx TestContext) { require.NotNil(t, viewedDash) // Clean up - err = adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + err = adminClient().Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) require.NoError(t, err) }) }) @@ -2097,6 +2158,8 @@ func runDashboardHttpTest(t *testing.T, ctx TestContext, foreignOrgCtx TestConte // Helper function to retrieve a dashboard via HTTP func getDashboardViaHTTP(t *testing.T, ctx *TestContext, dashboardPath string, user apis.User) (map[string]interface{}, error) { + t.Helper() + getResp := apis.DoRequest(ctx.Helper, apis.RequestParams{ User: user, Method: http.MethodGet, @@ -2194,7 +2257,7 @@ func testDashboardHttpUpdateMethods(t *testing.T, ctx TestContext, dashboardPath } // Test dashboard list API with complex permission scenarios -func runDashboardListTest(t *testing.T, ctx TestContext) { +func runDashboardListTests(t *testing.T, ctx TestContext) { t.Helper() // Make sure no dashboards exist before we start @@ -2223,8 +2286,8 @@ func runDashboardListTest(t *testing.T, ctx TestContext) { // Define a map of user types to their clients clients := map[string]struct { userClient *apis.K8sResourceClient - tokenClient *apis.K8sResourceClient folderClient *apis.K8sResourceClient + tokenClient *apis.K8sResourceClient }{ "Admin": { userClient: getResourceClient(t, ctx.Helper, ctx.AdminUser, getDashboardGVR()), @@ -2323,6 +2386,24 @@ func runDashboardListTest(t *testing.T, ctx TestContext) { folders := make([]*folder.Folder, len(folderConfigs)) folderDashboards := make([]*unstructured.Unstructured, len(folderConfigs)) + // Clean up + t.Cleanup(func() { + // Delete all root dashboards + for _, dash := range rootDashboards { + err := adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) + require.NoError(t, err) + } + + // Delete all folder dashboards and folders + for i, folder := range folders { + err := adminClient.Resource.Delete(context.Background(), folderDashboards[i].GetName(), v1.DeleteOptions{}) + require.NoError(t, err) + + err = adminFolderClient.Resource.Delete(context.Background(), folder.UID, v1.DeleteOptions{}) + require.NoError(t, err) + } + }) + // Create all test resources (folders, dashboards) in one loop for i, fc := range folderConfigs { // Create root dashboard @@ -2415,71 +2496,55 @@ func runDashboardListTest(t *testing.T, ctx TestContext) { // Test LIST operation for each identity for _, identity := range identities { - t.Run(fmt.Sprintf("LIST operation for %s", identity.Name), func(t *testing.T) { - // Get dashboards visible to this identity - clients := []apis.K8sResourceClient{ - *identity.DashboardClient, - *identity.FolderClient, - } + // Get dashboards visible to this identity + clients := []apis.K8sResourceClient{ + *identity.DashboardClient, + *identity.FolderClient, + } + for _, client := range clients { + t.Run(fmt.Sprintf("LIST operation for %s and %s", identity.Name, client.Args.GVR), func(t *testing.T) { + // Use the client to list all resources + dashList, err := client.Resource.List(context.Background(), v1.ListOptions{}) + require.NoError(t, err) - for _, client := range clients { - t.Run(fmt.Sprintf("LIST operation for %s", client.Args.GVR), func(t *testing.T) { - listOpts := v1.ListOptions{} - dashList, err := client.Resource.List(context.Background(), listOpts) + if len(dashList.Items) == 0 { + t.Logf("WARNING: Got empty dashboard list for %s", identity.Name) + } + + require.NotEmpty(t, dashList.Items) + + // Extract dashboard titles + dashTitles := make([]string, 0, len(dashList.Items)) + for _, dash := range dashList.Items { + meta, err := utils.MetaAccessor(&dash) require.NoError(t, err) - require.NotEmpty(t, dashList.Items) - // Extract dashboard titles - dashTitles := make([]string, 0, len(dashList.Items)) - for _, dash := range dashList.Items { - meta, err := utils.MetaAccessor(&dash) - require.NoError(t, err) + dashTitles = append(dashTitles, meta.FindTitle("")) + } - dashTitles = append(dashTitles, meta.FindTitle("")) - } + // Verify expectations + var expectedTitles []string + if client.Args.GVR == getDashboardGVR() { + expectedTitles = expectations[identity.Name] + } else { + expectedTitles = folderPermissions[identity.Name] + } + require.ElementsMatch(t, expectedTitles, dashTitles) - // Verify expectations - var expectedTitles []string - if client.Args.GVR == getDashboardGVR() { - expectedTitles = expectations[identity.Name] - } else { - expectedTitles = folderPermissions[identity.Name] - } - require.ElementsMatch(t, expectedTitles, dashTitles) - - // Verify all expected items are found - for _, expected := range expectedTitles { - found := false - for _, title := range dashTitles { - if title == expected { - found = true - break - } + // Verify all expected items are found + for _, expected := range expectedTitles { + found := false + for _, title := range dashTitles { + if title == expected { + found = true + break } - require.True(t, found, "%s should see dashboard '%s' but didn't", identity.Name, expected) } - }) - } - }) + require.True(t, found, "%s should see dashboard '%s' but didn't", identity.Name, expected) + } + }) + } } - - // Clean up - t.Run("Cleanup dashboards and folders", func(t *testing.T) { - // Delete all root dashboards - for _, dash := range rootDashboards { - err := adminClient.Resource.Delete(context.Background(), dash.GetName(), v1.DeleteOptions{}) - require.NoError(t, err) - } - - // Delete all folder dashboards and folders - for i, folder := range folders { - err := adminClient.Resource.Delete(context.Background(), folderDashboards[i].GetName(), v1.DeleteOptions{}) - require.NoError(t, err) - - err = adminFolderClient.Resource.Delete(context.Background(), folder.UID, v1.DeleteOptions{}) - require.NoError(t, err) - } - }) } func postHelper(t *testing.T, ctx *TestContext, path string, body interface{}, user apis.User) (map[string]interface{}, error) { From 0405e5fe521ddcfbf395cbbc10681a49d73ab076 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:32:18 +0100 Subject: [PATCH 087/101] Update dependency eslint-plugin-import to v2.32.0 (#108622) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 329 ++++++------------------------------------------------ 1 file changed, 36 insertions(+), 293 deletions(-) diff --git a/yarn.lock b/yarn.lock index c4f5f65234a..7105ef191a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8460,13 +8460,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-darwin-arm64@npm:1.11.4" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-darwin-arm64@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-darwin-arm64@npm:1.13.2" @@ -8481,13 +8474,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-darwin-x64@npm:1.11.4" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@swc/core-darwin-x64@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-darwin-x64@npm:1.13.2" @@ -8502,13 +8488,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.11.4" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@swc/core-linux-arm-gnueabihf@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-linux-arm-gnueabihf@npm:1.13.2" @@ -8523,13 +8502,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-linux-arm64-gnu@npm:1.11.4" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-arm64-gnu@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-linux-arm64-gnu@npm:1.13.2" @@ -8544,13 +8516,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-linux-arm64-musl@npm:1.11.4" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-arm64-musl@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-linux-arm64-musl@npm:1.13.2" @@ -8565,13 +8530,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-linux-x64-gnu@npm:1.11.4" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - "@swc/core-linux-x64-gnu@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-linux-x64-gnu@npm:1.13.2" @@ -8586,13 +8544,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-linux-x64-musl@npm:1.11.4" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - "@swc/core-linux-x64-musl@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-linux-x64-musl@npm:1.13.2" @@ -8607,13 +8558,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-win32-arm64-msvc@npm:1.11.4" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@swc/core-win32-arm64-msvc@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-win32-arm64-msvc@npm:1.13.2" @@ -8628,13 +8572,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-win32-ia32-msvc@npm:1.11.4" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@swc/core-win32-ia32-msvc@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-win32-ia32-msvc@npm:1.13.2" @@ -8649,13 +8586,6 @@ __metadata: languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.11.4": - version: 1.11.4 - resolution: "@swc/core-win32-x64-msvc@npm:1.11.4" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@swc/core-win32-x64-msvc@npm:1.13.2": version: 1.13.2 resolution: "@swc/core-win32-x64-msvc@npm:1.13.2" @@ -8709,53 +8639,7 @@ __metadata: languageName: node linkType: hard -"@swc/core@npm:^1.10.8": - version: 1.11.4 - resolution: "@swc/core@npm:1.11.4" - dependencies: - "@swc/core-darwin-arm64": "npm:1.11.4" - "@swc/core-darwin-x64": "npm:1.11.4" - "@swc/core-linux-arm-gnueabihf": "npm:1.11.4" - "@swc/core-linux-arm64-gnu": "npm:1.11.4" - "@swc/core-linux-arm64-musl": "npm:1.11.4" - "@swc/core-linux-x64-gnu": "npm:1.11.4" - "@swc/core-linux-x64-musl": "npm:1.11.4" - "@swc/core-win32-arm64-msvc": "npm:1.11.4" - "@swc/core-win32-ia32-msvc": "npm:1.11.4" - "@swc/core-win32-x64-msvc": "npm:1.11.4" - "@swc/counter": "npm:^0.1.3" - "@swc/types": "npm:^0.1.19" - peerDependencies: - "@swc/helpers": "*" - dependenciesMeta: - "@swc/core-darwin-arm64": - optional: true - "@swc/core-darwin-x64": - optional: true - "@swc/core-linux-arm-gnueabihf": - optional: true - "@swc/core-linux-arm64-gnu": - optional: true - "@swc/core-linux-arm64-musl": - optional: true - "@swc/core-linux-x64-gnu": - optional: true - "@swc/core-linux-x64-musl": - optional: true - "@swc/core-win32-arm64-msvc": - optional: true - "@swc/core-win32-ia32-msvc": - optional: true - "@swc/core-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@swc/helpers": - optional: true - checksum: 10/9dce8808f8c17be58c0e42f14d1c257db0418f7f426774e2f05794eb4b9011445c7ac13805e7ae0dad53b858720eabaffe0bcc5366696007e166bc4ab50fbe4d - languageName: node - linkType: hard - -"@swc/core@npm:^1.5.22": +"@swc/core@npm:^1.10.8, @swc/core@npm:^1.5.22": version: 1.13.2 resolution: "@swc/core@npm:1.13.2" dependencies: @@ -8830,16 +8714,7 @@ __metadata: languageName: node linkType: hard -"@swc/types@npm:^0.1.17, @swc/types@npm:^0.1.19": - version: 0.1.19 - resolution: "@swc/types@npm:0.1.19" - dependencies: - "@swc/counter": "npm:^0.1.3" - checksum: 10/693147cc9b23147164ddff9cb89477c369fbeb103319584779352a9ff1c72e0a70b97a89dfd97629040db8956d668d7b7a8fed328ffea46a3e8c18577e396994 - languageName: node - linkType: hard - -"@swc/types@npm:^0.1.23": +"@swc/types@npm:^0.1.17, @swc/types@npm:^0.1.23": version: 0.1.23 resolution: "@swc/types@npm:0.1.23" dependencies: @@ -11625,7 +11500,7 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": +"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8, array-includes@npm:^3.1.9": version: 3.1.9 resolution: "array-includes@npm:3.1.9" dependencies: @@ -11685,21 +11560,22 @@ __metadata: languageName: node linkType: hard -"array.prototype.findlastindex@npm:^1.2.5": - version: 1.2.5 - resolution: "array.prototype.findlastindex@npm:1.2.5" +"array.prototype.findlastindex@npm:^1.2.6": + version: 1.2.6 + resolution: "array.prototype.findlastindex@npm:1.2.6" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" + es-abstract: "npm:^1.23.9" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10/7c5c821f357cd53ab6cc305de8086430dd8d7a2485db87b13f843e868055e9582b1fd338f02338f67fc3a1603ceaf9610dd2a470b0b506f9d18934780f95b246 + es-object-atoms: "npm:^1.1.1" + es-shim-unscopables: "npm:^1.1.0" + checksum: 10/5ddb6420e820bef6ddfdcc08ce780d0fd5e627e97457919c27e32359916de5a11ce12f7c55073555e503856618eaaa70845d6ca11dcba724766f38eb1c22f7a2 languageName: node linkType: hard -"array.prototype.flat@npm:^1.3.1": +"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.3": version: 1.3.3 resolution: "array.prototype.flat@npm:1.3.3" dependencies: @@ -11711,18 +11587,6 @@ __metadata: languageName: node linkType: hard -"array.prototype.flat@npm:^1.3.2": - version: 1.3.2 - resolution: "array.prototype.flat@npm:1.3.2" - dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.2.0" - es-abstract: "npm:^1.22.1" - es-shim-unscopables: "npm:^1.0.0" - checksum: 10/d9d2f6f27584de92ec7995bc931103e6de722cd2498bdbfc4cba814fc3e52f056050a93be883018811f7c0a35875f5056584a0e940603a5e5934f0279896aebe - languageName: node - linkType: hard - "array.prototype.flatmap@npm:^1.3.2, array.prototype.flatmap@npm:^1.3.3": version: 1.3.3 resolution: "array.prototype.flatmap@npm:1.3.3" @@ -11961,18 +11825,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1, axios@npm:^1.7.9, axios@npm:^1.8.3, axios@npm:^1.9.0": - version: 1.10.0 - resolution: "axios@npm:1.10.0" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10/d43c80316a45611fd395743e15d16ea69a95f2b7f7095f2bb12cb78f9ca0a905194a02e52a3bf4e0db9f85fd1186d6c690410644c10ecd8bb0a468e57c2040e4 - languageName: node - linkType: hard - -"axios@npm:^1.6.1": +"axios@npm:^1, axios@npm:^1.6.1, axios@npm:^1.7.9, axios@npm:^1.8.3, axios@npm:^1.9.0": version: 1.11.0 resolution: "axios@npm:1.11.0" dependencies: @@ -13714,16 +13567,7 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:^1.5.0, convert-source-map@npm:^1.6.0": - version: 1.8.0 - resolution: "convert-source-map@npm:1.8.0" - dependencies: - safe-buffer: "npm:~5.1.1" - checksum: 10/985d974a2d33e1a2543ada51c93e1ba2f73eaed608dc39f229afc78f71dcc4c8b7d7c684aa647e3c6a3a204027444d69e53e169ce94e8d1fa8d7dee80c9c8fed - languageName: node - linkType: hard - -"convert-source-map@npm:^1.7.0": +"convert-source-map@npm:^1.5.0, convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" checksum: 10/dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 @@ -15032,7 +14876,7 @@ __metadata: languageName: node linkType: hard -"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1": +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -15870,65 +15714,6 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.22.1": - version: 1.23.9 - resolution: "es-abstract@npm:1.23.9" - dependencies: - array-buffer-byte-length: "npm:^1.0.2" - arraybuffer.prototype.slice: "npm:^1.0.4" - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - data-view-buffer: "npm:^1.0.2" - data-view-byte-length: "npm:^1.0.2" - data-view-byte-offset: "npm:^1.0.1" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-set-tostringtag: "npm:^2.1.0" - es-to-primitive: "npm:^1.3.0" - function.prototype.name: "npm:^1.1.8" - get-intrinsic: "npm:^1.2.7" - get-proto: "npm:^1.0.0" - get-symbol-description: "npm:^1.1.0" - globalthis: "npm:^1.0.4" - gopd: "npm:^1.2.0" - has-property-descriptors: "npm:^1.0.2" - has-proto: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - internal-slot: "npm:^1.1.0" - is-array-buffer: "npm:^3.0.5" - is-callable: "npm:^1.2.7" - is-data-view: "npm:^1.0.2" - is-regex: "npm:^1.2.1" - is-shared-array-buffer: "npm:^1.0.4" - is-string: "npm:^1.1.1" - is-typed-array: "npm:^1.1.15" - is-weakref: "npm:^1.1.0" - math-intrinsics: "npm:^1.1.0" - object-inspect: "npm:^1.13.3" - object-keys: "npm:^1.1.1" - object.assign: "npm:^4.1.7" - own-keys: "npm:^1.0.1" - regexp.prototype.flags: "npm:^1.5.3" - safe-array-concat: "npm:^1.1.3" - safe-push-apply: "npm:^1.0.0" - safe-regex-test: "npm:^1.1.0" - set-proto: "npm:^1.0.0" - string.prototype.trim: "npm:^1.2.10" - string.prototype.trimend: "npm:^1.0.9" - string.prototype.trimstart: "npm:^1.0.8" - typed-array-buffer: "npm:^1.0.3" - typed-array-byte-length: "npm:^1.0.3" - typed-array-byte-offset: "npm:^1.0.4" - typed-array-length: "npm:^1.0.7" - unbox-primitive: "npm:^1.1.0" - which-typed-array: "npm:^1.1.18" - checksum: 10/31a321966d760d88fc2ed984104841b42f4f24fc322b246002b9be0af162e03803ee41fcc3cf8be89e07a27ba3033168f877dd983703cb81422ffe5322a27582 - languageName: node - linkType: hard - "es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1": version: 1.0.1 resolution: "es-define-property@npm:1.0.1" @@ -15995,16 +15780,7 @@ __metadata: languageName: node linkType: hard -"es-shim-unscopables@npm:^1.0.0": - version: 1.0.2 - resolution: "es-shim-unscopables@npm:1.0.2" - dependencies: - hasown: "npm:^2.0.0" - checksum: 10/6d3bf91f658a27cc7217cd32b407a0d714393a84d125ad576319b9e83a893bea165cf41270c29e9ceaa56d3cf41608945d7e2a2c31fd51c0009b0c31402b91c7 - languageName: node - linkType: hard - -"es-shim-unscopables@npm:^1.0.2": +"es-shim-unscopables@npm:^1.0.2, es-shim-unscopables@npm:^1.1.0": version: 1.1.0 resolution: "es-shim-unscopables@npm:1.1.0" dependencies: @@ -16266,44 +16042,44 @@ __metadata: languageName: node linkType: hard -"eslint-module-utils@npm:^2.12.0": - version: 2.12.0 - resolution: "eslint-module-utils@npm:2.12.0" +"eslint-module-utils@npm:^2.12.1": + version: 2.12.1 + resolution: "eslint-module-utils@npm:2.12.1" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee + checksum: 10/bd25d6610ec3abaa50e8f1beb0119541562bbb8dd02c035c7e887976fe1e0c5dd8175f4607ca8d86d1146df24d52a071bd3d1dd329f6902bd58df805a8ca16d3 languageName: node linkType: hard "eslint-plugin-import@npm:^2.31.0": - version: 2.31.0 - resolution: "eslint-plugin-import@npm:2.31.0" + version: 2.32.0 + resolution: "eslint-plugin-import@npm:2.32.0" dependencies: "@rtsao/scc": "npm:^1.1.0" - array-includes: "npm:^3.1.8" - array.prototype.findlastindex: "npm:^1.2.5" - array.prototype.flat: "npm:^1.3.2" - array.prototype.flatmap: "npm:^1.3.2" + array-includes: "npm:^3.1.9" + array.prototype.findlastindex: "npm:^1.2.6" + array.prototype.flat: "npm:^1.3.3" + array.prototype.flatmap: "npm:^1.3.3" debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.12.0" + eslint-module-utils: "npm:^2.12.1" hasown: "npm:^2.0.2" - is-core-module: "npm:^2.15.1" + is-core-module: "npm:^2.16.1" is-glob: "npm:^4.0.3" minimatch: "npm:^3.1.2" object.fromentries: "npm:^2.0.8" object.groupby: "npm:^1.0.3" - object.values: "npm:^1.2.0" + object.values: "npm:^1.2.1" semver: "npm:^6.3.1" - string.prototype.trimend: "npm:^1.0.8" + string.prototype.trimend: "npm:^1.0.9" tsconfig-paths: "npm:^3.15.0" peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - checksum: 10/6b76bd009ac2db0615d9019699d18e2a51a86cb8c1d0855a35fb1b418be23b40239e6debdc6e8c92c59f1468ed0ea8d7b85c817117a113d5cc225be8a02ad31c + checksum: 10/1bacf4967e9ebf99e12176a795f0d6d3a87d1c9a030c2207f27b267e10d96a1220be2647504c7fc13ab543cdf13ffef4b8f5620e0447032dba4ff0d3922f7c9e languageName: node linkType: hard @@ -19978,7 +19754,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0, is-core-module@npm:^2.5.0": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1, is-core-module@npm:^2.5.0": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -20456,15 +20232,6 @@ __metadata: languageName: node linkType: hard -"is-weakref@npm:^1.1.0": - version: 1.1.0 - resolution: "is-weakref@npm:1.1.0" - dependencies: - call-bound: "npm:^1.0.2" - checksum: 10/89e627cc1763ea110574bb408fcf060ede47e70437d9278858bc939e3b3f7e4b7c558610b733da5f2ad6084d9f12b9c714b011ccf3fa771ec87e221c22bed910 - languageName: node - linkType: hard - "is-weakset@npm:^2.0.3": version: 2.0.4 resolution: "is-weakset@npm:2.0.4" @@ -20665,7 +20432,7 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.0.2": +"istanbul-reports@npm:^3.0.2, istanbul-reports@npm:^3.1.3": version: 3.1.7 resolution: "istanbul-reports@npm:3.1.7" dependencies: @@ -20675,16 +20442,6 @@ __metadata: languageName: node linkType: hard -"istanbul-reports@npm:^3.1.3": - version: 3.1.5 - resolution: "istanbul-reports@npm:3.1.5" - dependencies: - html-escaper: "npm:^2.0.0" - istanbul-lib-report: "npm:^3.0.0" - checksum: 10/1fc20a133f6dbd846e7bf3dc6d85edf2b3c047c47142cd796c38717aef976195d2c0fb0399dd609c3ffac2ca43244dc15ce4ac34064d21e2d34d387df747dafb - languageName: node - linkType: hard - "iterator.prototype@npm:^1.1.4": version: 1.1.5 resolution: "iterator.prototype@npm:1.1.5" @@ -24432,7 +24189,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1": +"object.values@npm:^1.1.6, object.values@npm:^1.2.1": version: 1.2.1 resolution: "object.values@npm:1.2.1" dependencies: @@ -30104,7 +29861,7 @@ __metadata: languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9": +"string.prototype.trimend@npm:^1.0.9": version: 1.0.9 resolution: "string.prototype.trimend@npm:1.0.9" dependencies: @@ -32769,20 +32526,6 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.18": - version: 1.1.18 - resolution: "which-typed-array@npm:1.1.18" - dependencies: - available-typed-arrays: "npm:^1.0.7" - call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - for-each: "npm:^0.3.3" - gopd: "npm:^1.2.0" - has-tostringtag: "npm:^1.0.2" - checksum: 10/11eed801b2bd08cdbaecb17aff381e0fb03526532f61acc06e6c7b9370e08062c33763a51f27825f13fdf34aabd0df6104007f4e8f96e6eaef7db0ce17a26d6e - languageName: node - linkType: hard - "which@npm:^1.2.12, which@npm:^1.2.14, which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" From 5febfc7d9b35028abb231a0e94606e8543288eef Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 25 Jul 2025 09:37:15 +0100 Subject: [PATCH 088/101] Playwright: Fix GHA summary step (#108587) * Playwright: Fix GHA summary step * add failure to test * remove failing test --- .github/workflows/pr-e2e-tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml index 5e7d116d0c2..215a4016344 100644 --- a/.github/workflows/pr-e2e-tests.yml +++ b/.github/workflows/pr-e2e-tests.yml @@ -286,6 +286,7 @@ jobs: /home/bench/tests/playwright-results.json - name: Upload HTML report + id: upload-html uses: actions/upload-artifact@v4 with: name: playwright-html-${{ github.run_number }} @@ -304,10 +305,13 @@ jobs: - name: Show test results env: FAILED: ${{ steps.check-jobs.outputs.any-failed }} + REPORT_URL: ${{ steps.upload-html.outputs.artifact-url }} # sed removes the leading `../../src/` from the paths run: | - npx playwright merge-reports --reporter list ./all-blob-reports | sed 's|\(\.\./\)\{1,\}src/|/|g' + npx playwright merge-reports --reporter list ./blobs | sed 's|\(\.\./\)\{1,\}src/|/|g' if [ "$FAILED" = "true" ]; then + echo "" + echo "Download the test report from $REPORT_URL" exit 1 fi From 02f9ebc62ea3b3eab008a202b9321265883e5e38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:58:39 +0100 Subject: [PATCH 089/101] Update dependency @grafana/assistant to v0.0.10 (#108660) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c76a660b530..d9893f9a486 100644 --- a/package.json +++ b/package.json @@ -272,7 +272,7 @@ "@formatjs/intl-durationformat": "^0.7.0", "@glideapps/glide-data-grid": "^6.0.0", "@grafana/alerting": "workspace:*", - "@grafana/assistant": "0.0.8", + "@grafana/assistant": "0.0.10", "@grafana/aws-sdk": "0.7.1", "@grafana/azure-sdk": "0.0.7", "@grafana/data": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 7105ef191a9..f71cb2c775f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3073,9 +3073,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/assistant@npm:0.0.8": - version: 0.0.8 - resolution: "@grafana/assistant@npm:0.0.8" +"@grafana/assistant@npm:0.0.10": + version: 0.0.10 + resolution: "@grafana/assistant@npm:0.0.10" peerDependencies: "@grafana/data": ">=12.1.0" "@grafana/runtime": ">=12.1.0" @@ -3083,7 +3083,7 @@ __metadata: "@grafana/ui": ">=12.1.0" react: ">=18.0.0" rxjs: ">=7.0.0" - checksum: 10/2fd503b727716cb5bd3d25f16a50e177e58c9ea96712b66e785f1f2348e4f211b2d2250d8542420458ac2fb2f4f474f403850326bef9a99895990f63d009c640 + checksum: 10/4b5f0ea03e8cebba07285d4f2ea642211acde0e70207e11456640169fd32d6c550fd3be9256ca41266438237d129b28446981efaa2399e20c2cce064080c1e04 languageName: node linkType: hard @@ -18157,7 +18157,7 @@ __metadata: "@formatjs/intl-durationformat": "npm:^0.7.0" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana/alerting": "workspace:*" - "@grafana/assistant": "npm:0.0.8" + "@grafana/assistant": "npm:0.0.10" "@grafana/aws-sdk": "npm:0.7.1" "@grafana/azure-sdk": "npm:0.0.7" "@grafana/data": "workspace:*" From 6b3d8b0a30c7dd557583090848f6cf5db6c0f554 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 25 Jul 2025 10:10:28 +0100 Subject: [PATCH 090/101] Chore: Bump axios (#108663) From c21617a4462c8c0dc5a9257fed94b80f1e0208dd Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 25 Jul 2025 11:32:07 +0200 Subject: [PATCH 091/101] Make: Use air as the default for make run (#108652) --- Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index db029935738..1d058780a48 100644 --- a/Makefile +++ b/Makefile @@ -263,8 +263,8 @@ build-plugin-go: ## Build decoupled plugins build: build-go build-js ## Build backend and frontend. .PHONY: run -run: ## Build and run web server on filesystem changes. See /.bra.toml for configuration. - $(bra) run +run: ## Build and run backend, and watch for changes. See .air.toml for configuration. + $(air) -c .air.toml .PHONY: run-go run-go: ## Build and run web server immediately. @@ -275,9 +275,9 @@ run-go: ## Build and run web server immediately. run-frontend: deps-js ## Fetch js dependencies and watch frontend for rebuild yarn start -.PHONY: run-air -run-air: ## Build and run backend, and watch for changes. See .air.toml for configuration. - $(air) -c .air.toml +.PHONY: run-bra +run-air: ## [Deprecated] Build and run web server on filesystem changes. See /.bra.toml for configuration. + $(bra) run .PHONY: frontend-service-check frontend-service-check: From fa8b631a878031762ac99c1ae046a8509817d470 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 25 Jul 2025 11:45:09 +0200 Subject: [PATCH 092/101] Chore: Improve prometheus language provider documentation (#108661) * improve documentation * fix * betterer --- .betterer.results | 3 +- .../src/language_provider.ts | 76 +++++++++++++++---- .../src/resource_clients.test.ts | 25 ++++++ .../src/resource_clients.ts | 29 ++++++- 4 files changed, 114 insertions(+), 19 deletions(-) diff --git a/.betterer.results b/.betterer.results index 86db65d8f44..6429e7b67b5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -453,7 +453,8 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], "packages/grafana-prometheus/src/language_utils.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 3ff374771d7..0419295ab16 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -515,19 +515,6 @@ export default class PromQlLanguageProvider extends LanguageProvider implements }; } -export interface PrometheusLanguageProviderInterface - extends PrometheusBaseLanguageProvider, - PrometheusLegacyLanguageProvider { - retrieveMetricsMetadata: () => PromMetricsMetadata; - retrieveHistogramMetrics: () => string[]; - retrieveMetrics: () => string[]; - retrieveLabelKeys: () => string[]; - - queryMetricsMetadata: (limit?: number) => Promise; - queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: number) => Promise; - queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise; -} - /** * Modern implementation of the Prometheus language provider that abstracts API endpoint selection. * @@ -535,12 +522,73 @@ export interface PrometheusLanguageProviderInterface * - Automatically selects the most efficient API endpoint based on Prometheus version and configuration * - Supports both labels and series endpoints for backward compatibility * - Handles match[] parameters for filtering time series data - * - Implements automatic request limiting (default: 40_000 series) + * - Implements automatic request limiting (default: 40,000 series if not configured otherwise) * - Provides unified interface for both modern and legacy Prometheus versions + * - Provides caching mechanism based on time range, limit, and match parameters * * @see LabelsApiClient For modern Prometheus versions using the labels API * @see SeriesApiClient For legacy Prometheus versions using the series API */ +export interface PrometheusLanguageProviderInterface + extends PrometheusBaseLanguageProvider, + PrometheusLegacyLanguageProvider { + /** + * Initializes the language provider by fetching metrics, label keys, and metrics metadata using Resource Clients. + * All calls use the limit parameter from datasource configuration (default: 40,000 if not set). + * + * For backward compatibility, it calls _backwardCompatibleStart. + * Some places still rely on deprecated fields. Until we replace them, we need _backwardCompatibleStart method. + */ + start: (timeRange?: TimeRange) => Promise; + + /** + * Returns already cached metrics metadata including type and help information. + * If there is no cached metadata, it returns an empty object. + * To get fresh metadata, use queryMetricsMetadata instead. + */ + retrieveMetricsMetadata: () => PromMetricsMetadata; + + /** + * Returns already cached list of histogram metrics (identified by '_bucket' suffix). + * If there are no cached histogram metrics, it returns an empty array. + */ + retrieveHistogramMetrics: () => string[]; + + /** + * Returns already cached list of all available metric names. + * If there are no cached metrics, it returns an empty array. + */ + retrieveMetrics: () => string[]; + + /** + * Returns already cached list of available label keys. + * If there are no cached label keys, it returns an empty array. + */ + retrieveLabelKeys: () => string[]; + + /** + * Fetches fresh metrics metadata from Prometheus with optional limit. + * Uses datasource's default limit if not specified. + */ + queryMetricsMetadata: (limit?: number) => Promise; + + /** + * Queries Prometheus for label keys within time range, optionally filtered by match selector. + * Automatically selects labels or series endpoint based on datasource configuration. + * If no limit is provided, uses the datasource's default limit configuration. + * Use zero (0) to fetch all label keys, but this might return huge amounts of data. + */ + queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: number) => Promise; + + /** + * Queries Prometheus for values of a specific label key, optionally filtered by match selector. + * Automatically selects labels or series endpoint based on datasource configuration. + * If no limit is provided, uses the datasource's default limit configuration. + * Use zero (0) to fetch all label values, but this might return huge amounts of data. + */ + queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise; +} + export class PrometheusLanguageProvider extends PromQlLanguageProvider implements PrometheusLanguageProviderInterface { private _metricsMetadata?: PromMetricsMetadata; private _resourceClient?: ResourceApiClient; diff --git a/packages/grafana-prometheus/src/resource_clients.test.ts b/packages/grafana-prometheus/src/resource_clients.test.ts index f919615b44e..4a2790227ee 100644 --- a/packages/grafana-prometheus/src/resource_clients.test.ts +++ b/packages/grafana-prometheus/src/resource_clients.test.ts @@ -836,6 +836,11 @@ describe('BaseResourceClient', () => { constructor() { super(mockRequest, mockDatasource); } + + // Expose protected method for testing + public testGetEffectiveLimit(limit?: number): number { + return this.getEffectiveLimit(limit); + } } let client: TestBaseResourceClient; @@ -845,6 +850,26 @@ describe('BaseResourceClient', () => { client = new TestBaseResourceClient(); }); + describe('getEffectiveLimit', () => { + it('should return the provided limit when a number is given', () => { + expect(client.testGetEffectiveLimit(1000)).toBe(1000); + expect(client.testGetEffectiveLimit(500)).toBe(500); + expect(client.testGetEffectiveLimit(100000)).toBe(100000); + }); + + it('should return 0 when limit is 0 (valid for unlimited)', () => { + expect(client.testGetEffectiveLimit(0)).toBe(0); + }); + + it('should return datasource seriesLimit when limit is undefined', () => { + expect(client.testGetEffectiveLimit(undefined)).toBe(DEFAULT_SERIES_LIMIT); + }); + + it('should return datasource seriesLimit when no limit is provided', () => { + expect(client.testGetEffectiveLimit()).toBe(DEFAULT_SERIES_LIMIT); + }); + }); + describe('querySeries', () => { const mockTimeRange = { from: dateTime(1681300292392), diff --git a/packages/grafana-prometheus/src/resource_clients.ts b/packages/grafana-prometheus/src/resource_clients.ts index cc59213cf47..2eb4a6b2ce0 100644 --- a/packages/grafana-prometheus/src/resource_clients.ts +++ b/packages/grafana-prometheus/src/resource_clients.ts @@ -43,8 +43,16 @@ export abstract class BaseResourceClient { this.seriesLimit = this.datasource.seriesLimit; } + /** + * Returns the effective limit to use for API requests. + * Uses the provided limit if specified, otherwise falls back to the datasource's configured series limit. + * When zero is provided, it returns zero (which means no limit in Prometheus API). + * + * @param {number} [limit] - Optional limit parameter from the API call + * @returns {number} The limit to use - either the provided limit or datasource's default series limit + */ protected getEffectiveLimit(limit?: number): number { - return limit || this.seriesLimit; + return limit ?? this.seriesLimit; } protected async requestLabels( @@ -93,10 +101,23 @@ export class LabelsApiClient extends BaseResourceClient implements ResourceApiCl this.labelKeys = await this.queryLabelKeys(timeRange); }; - public queryMetrics = async (timeRange: TimeRange): Promise<{ metrics: string[]; histogramMetrics: string[] }> => { - this.metrics = await this.queryLabelValues(timeRange, METRIC_LABEL); + /** + * Fetches all available metrics from Prometheus using the labels values endpoint for __name__. + * Also processes and identifies histogram metrics (those ending with '_bucket'). + * Results are cached and stored in the client instance for future use. + * + * @param {TimeRange} timeRange - Time range to search for metrics + * @param {number} [limit] - Optional maximum number of metrics to return, uses datasource default if not specified + * @returns {Promise<{metrics: string[], histogramMetrics: string[]}>} Object containing all metrics and filtered histogram metrics + */ + public queryMetrics = async ( + timeRange: TimeRange, + limit?: number + ): Promise<{ metrics: string[]; histogramMetrics: string[] }> => { + const effectiveLimit = this.getEffectiveLimit(limit); + this.metrics = await this.queryLabelValues(timeRange, METRIC_LABEL, undefined, effectiveLimit); this.histogramMetrics = processHistogramMetrics(this.metrics); - this._cache.setLabelValues(timeRange, undefined, DEFAULT_SERIES_LIMIT, this.metrics); + this._cache.setLabelValues(timeRange, undefined, effectiveLimit, this.metrics); return { metrics: this.metrics, histogramMetrics: this.histogramMetrics }; }; From 11b49cf7bdd20088edc75b27af213c3c57db2c35 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 25 Jul 2025 10:54:07 +0100 Subject: [PATCH 093/101] Playwright: Some tweaks to improve stability (#108667) playwright stability tweaks --- .../dashboards-edit-panel-title-description.spec.ts | 6 ++++-- .../dashboards-suite/dashboard-timepicker.spec.ts | 3 +++ e2e-playwright/various-suite/frontend-sandbox-app.spec.ts | 8 ++++---- playwright.config.ts | 3 +++ 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts index 30373362528..425f01db823 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-edit-panel-title-description.spec.ts @@ -51,8 +51,10 @@ test.describe( // Get the tooltip ID from the aria-describedby attribute const tooltipId = await descriptionIcon.getAttribute('aria-describedby'); - const tooltip = page.locator(`[id="${tooltipId}"]`); - await expect(tooltip).toHaveText(`${newDescription}\n`); + await expect(async () => { + const tooltip = page.locator(`[id="${tooltipId}"]`); + await expect(tooltip).toHaveText(`${newDescription}\n`); + }).toPass(); }); } ); diff --git a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts index ac505aa9cd4..e57c56b5afe 100644 --- a/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts +++ b/e2e-playwright/dashboards-suite/dashboard-timepicker.spec.ts @@ -42,6 +42,9 @@ test.describe( await page.getByTestId(selectors.components.TimeZonePicker.containerV2).click(); await page.getByRole('option', { name: 'Asia/Tokyo' }).click(); await page.getByTestId(selectors.components.UserProfile.preferencesSaveButton).click(); + // wait for the page to reload before trying to navigate, otherwise this can cause flakes + // see e.g. https://github.com/microsoft/playwright/issues/21451#issuecomment-1502251404 + await page.waitForURL('/profile'); // Open dashboard with time range from 8th to end of 10th. // Will be Tokyo time because of above preference diff --git a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts index 6ec59ded434..dfa19753b15 100644 --- a/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts +++ b/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @@ -31,7 +31,7 @@ test.describe( await expect(sandboxDiv).toBeHidden(); const appPage = page.getByTestId('sandbox-app-test-page-one'); - await expect(appPage).toBeVisible({ timeout: 10000 }); + await expect(appPage).toBeVisible(); }); test('Loads the app configuration without the sandbox div wrapper', async ({ page }) => { @@ -41,7 +41,7 @@ test.describe( await expect(sandboxDiv).toBeHidden(); const configPage = page.getByTestId('sandbox-app-test-config-page'); - await expect(configPage).toBeVisible({ timeout: 10000 }); + await expect(configPage).toBeVisible(); }); }); @@ -59,7 +59,7 @@ test.describe( await expect(sandboxDiv).toBeVisible(); const appPage = page.getByTestId('sandbox-app-test-page-one'); - await expect(appPage).toBeVisible({ timeout: 10000 }); + await expect(appPage).toBeVisible(); }); test('Loads the app configuration with the sandbox div wrapper', async ({ page }) => { @@ -69,7 +69,7 @@ test.describe( await expect(sandboxDiv).toBeVisible(); const configPage = page.getByTestId('sandbox-app-test-config-page'); - await expect(configPage).toBeVisible({ timeout: 10000 }); + await expect(configPage).toBeVisible(); }); }); }); diff --git a/playwright.config.ts b/playwright.config.ts index 94281dbd2d8..b569bc6feea 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,6 +16,9 @@ export default defineConfig({ reporter: [ ['html'], // pretty ], + expect: { + timeout: 10_000, + }, use: { baseURL: process.env.GRAFANA_URL ?? DEFAULT_URL, trace: 'retain-on-failure', From 7e8a471b0843a5f823b2e4ad63001f7726f80dc8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 10:56:07 +0000 Subject: [PATCH 094/101] Update dependency ol to v10.6.1 (#98200) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index d9893f9a486..fa37681c8ac 100644 --- a/package.json +++ b/package.json @@ -372,7 +372,7 @@ "moveable": "0.53.0", "nanoid": "^5.0.9", "node-forge": "^1.3.1", - "ol": "10.6.0", + "ol": "10.6.1", "ol-ext": "4.0.33", "pluralize": "^8.0.0", "prismjs": "1.30.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 8029328d12e..99cf6ee0f52 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -73,7 +73,7 @@ "marked-mangle": "1.1.11", "moment": "2.30.1", "moment-timezone": "0.5.47", - "ol": "10.6.0", + "ol": "10.6.1", "papaparse": "5.5.3", "react-use": "17.6.0", "rxjs": "7.8.2", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 6196a2a38a0..7fd647aee7e 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -99,7 +99,7 @@ "micro-memoize": "^4.1.2", "moment": "2.30.1", "monaco-editor": "0.34.1", - "ol": "10.6.0", + "ol": "10.6.1", "prismjs": "1.30.0", "rc-cascader": "3.34.0", "rc-drawer": "7.3.0", diff --git a/yarn.lock b/yarn.lock index f71cb2c775f..6b0cdd0997d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3149,7 +3149,7 @@ __metadata: marked-mangle: "npm:1.1.11" moment: "npm:2.30.1" moment-timezone: "npm:0.5.47" - ol: "npm:10.6.0" + ol: "npm:10.6.1" papaparse: "npm:5.5.3" react: "npm:18.3.1" react-dom: "npm:18.3.1" @@ -3832,7 +3832,7 @@ __metadata: monaco-editor: "npm:0.34.1" msw: "npm:^2.10.2" msw-storybook-addon: "npm:^2.0.5" - ol: "npm:10.6.0" + ol: "npm:10.6.1" prismjs: "npm:1.30.0" process: "npm:^0.11.10" rc-cascader: "npm:3.34.0" @@ -18394,7 +18394,7 @@ __metadata: node-forge: "npm:^1.3.1" node-notifier: "npm:10.0.1" nx: "npm:20.7.1" - ol: "npm:10.6.0" + ol: "npm:10.6.1" ol-ext: "npm:4.0.33" openapi-types: "npm:^12.1.3" pa11y-ci: "npm:^3.1.0" @@ -24217,16 +24217,16 @@ __metadata: languageName: node linkType: hard -"ol@npm:10.6.0": - version: 10.6.0 - resolution: "ol@npm:10.6.0" +"ol@npm:10.6.1": + version: 10.6.1 + resolution: "ol@npm:10.6.1" dependencies: "@types/rbush": "npm:4.0.0" earcut: "npm:^3.0.0" geotiff: "npm:^2.1.3" pbf: "npm:4.0.1" rbush: "npm:^4.0.0" - checksum: 10/942fe6c0b55b24a6b1538d78ad221962476bfcba5b1316f427235d50aa817ca5ed805e8d3b5c6129b5da245ba0b66cd8bf359403230bb939648a1a78bffb0ca3 + checksum: 10/f112a2907ff8644b19cf29fcabdf34a4c124828f6efb9687d27a12ad5b76588cfda436b9100147d29875eaa1e4543d788514db35ecdbaaa7d19e4c4bd9e183a8 languageName: node linkType: hard From d25d926462fd59339ed0b6664377760093cef372 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 25 Jul 2025 13:42:16 +0200 Subject: [PATCH 095/101] Advisor: Fix paginated requests for checks (#108583) --- .../pkg/app/checkscheduler/checkscheduler.go | 39 ++++++++++-- .../app/checkscheduler/checkscheduler_test.go | 63 +++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go index 9e382c5315f..92fe70fe192 100644 --- a/apps/advisor/pkg/app/checkscheduler/checkscheduler.go +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler.go @@ -102,6 +102,12 @@ func (r *Runner) Run(ctx context.Context) error { } else { lastCreated = time.Now() } + } else { + // Run an initial cleanup to remove old checks + err = r.cleanupChecks(ctxWithoutCancel, logger) + if err != nil { + logger.Error("Error cleaning up old check reports", "error", err) + } } } @@ -132,17 +138,35 @@ func (r *Runner) Run(ctx context.Context) error { } } +func (r *Runner) listChecks(ctx context.Context, logger logging.Logger) ([]resource.Object, error) { + list, err := r.client.List(ctx, r.namespace, resource.ListOptions{}) + if err != nil { + return nil, err + } + + checks := list.GetItems() + for list.GetContinue() != "" { + logger.Debug("List has continue token, listing next page", "continue", list.GetContinue()) + list, err = r.client.List(ctx, r.namespace, resource.ListOptions{Continue: list.GetContinue()}) + if err != nil { + return nil, err + } + checks = append(checks, list.GetItems()...) + } + return checks, nil +} + // checkLastCreated returns the creation time of the last check created // regardless of its ID. This assumes that the checks are created in batches // so a batch will have a similar creation time. // In case it finds an unprocessed check from a previous run, it will set it to error. func (r *Runner) checkLastCreated(ctx context.Context, log logging.Logger) (time.Time, error) { - list, err := r.client.List(ctx, r.namespace, resource.ListOptions{}) + checkList, err := r.listChecks(ctx, log) if err != nil { return time.Time{}, err } lastCreated := time.Time{} - for _, item := range list.GetItems() { + for _, item := range checkList { itemCreated := item.GetCreationTimestamp().Time if itemCreated.After(lastCreated) { lastCreated = itemCreated @@ -209,14 +233,16 @@ func (r *Runner) createChecks(ctx context.Context, logger logging.Logger) error // cleanupChecks deletes the olders checks if the number of checks exceeds the limit. func (r *Runner) cleanupChecks(ctx context.Context, logger logging.Logger) error { - list, err := r.client.List(ctx, r.namespace, resource.ListOptions{Limit: -1}) + checkList, err := r.listChecks(ctx, logger) if err != nil { return err } + logger.Debug("Cleaning up checks", "numChecks", len(checkList)) + // organize checks by type checksByType := map[string][]resource.Object{} - for _, check := range list.GetItems() { + for _, check := range checkList { labels := check.GetLabels() checkType, ok := labels[checks.TypeLabel] if !ok { @@ -226,8 +252,10 @@ func (r *Runner) cleanupChecks(ctx context.Context, logger logging.Logger) error checksByType[checkType] = append(checksByType[checkType], check) } - for _, checks := range checksByType { + for checkType, checks := range checksByType { + logger.Debug("Checking checks", "checkType", checkType, "numChecks", len(checks)) if len(checks) > r.maxHistory { + logger.Debug("Deleting old checks", "checkType", checkType, "maxHistory", r.maxHistory, "numChecks", len(checks)) // Sort checks by creation time sort.Slice(checks, func(i, j int) bool { ti := checks[i].GetCreationTimestamp().Time @@ -242,6 +270,7 @@ func (r *Runner) cleanupChecks(ctx context.Context, logger logging.Logger) error if err != nil { return fmt.Errorf("error deleting check: %w", err) } + logger.Debug("Deleted check", "check", check.GetStaticMetadata().Identifier()) } } } diff --git a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go index 69b4e809b4c..de522be7adc 100644 --- a/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go +++ b/apps/advisor/pkg/app/checkscheduler/checkscheduler_test.go @@ -103,6 +103,69 @@ func TestRunner_checkLastCreated_UnprocessedCheck(t *testing.T) { assert.Equal(t, expectedAnnotations, patchOperation.Value) } +func TestRunner_checkLastCreated_PaginatedResponse(t *testing.T) { + // Create checks with different creation times + past := time.Now().Add(-1 * time.Hour) + now := time.Now() + + mockClient := &MockClient{ + listFunc: func(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) { + if options.Continue == "" { + // First page - return oldest and middle checks with continue token + return &advisorv0alpha1.CheckList{ + ListMeta: metav1.ListMeta{ + Continue: "continue-token-123", + }, + Items: []advisorv0alpha1.Check{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "check-1", + CreationTimestamp: metav1.NewTime(past), + Annotations: map[string]string{ + checks.StatusAnnotation: "completed", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "check-2", + CreationTimestamp: metav1.NewTime(past), + Annotations: map[string]string{ + checks.StatusAnnotation: "completed", + }, + }, + }, + }, + }, nil + } + // Second page - verify continue token is passed and return newest check + assert.Equal(t, "continue-token-123", options.Continue) + return &advisorv0alpha1.CheckList{ + Items: []advisorv0alpha1.Check{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "check-3", + CreationTimestamp: metav1.NewTime(now), + Annotations: map[string]string{ + checks.StatusAnnotation: "completed", + }, + }, + }, + }, + }, nil + }, + } + + runner := &Runner{ + client: mockClient, + log: &logging.NoOpLogger{}, + } + + lastCreated, err := runner.checkLastCreated(context.Background(), &logging.NoOpLogger{}) + assert.NoError(t, err) + assert.Equal(t, now.Truncate(time.Second), lastCreated.Truncate(time.Second)) +} + func TestRunner_createChecks_ErrorOnCreate(t *testing.T) { mockCheckService := &MockCheckService{checks: []checks.Check{&mockCheck{id: "check-1"}}} From 6747b6082271ce1be0555eca4aa6a3f97969d4fc Mon Sep 17 00:00:00 2001 From: Will Browne Date: Fri, 25 Jul 2025 13:02:05 +0100 Subject: [PATCH 096/101] Plugins: Remove pluginsDetailsRightPanel feature toggle (#107321) remove pluginsDetailsRightPanel feature toggle --- .../configure-grafana/feature-toggles/index.md | 1 - packages/grafana-data/src/types/featureToggles.gen.ts | 5 ----- pkg/services/featuremgmt/registry.go | 8 -------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 3 ++- .../plugins/admin/components/PluginDetailsBody.tsx | 2 +- .../admin/components/PluginDetailsPage.test.tsx | 11 +---------- .../plugins/admin/components/PluginDetailsPage.tsx | 9 ++------- .../plugins/admin/components/PluginSubtitle.tsx | 3 +-- .../plugins/admin/hooks/usePluginDetailsTabs.tsx | 2 +- .../plugins/admin/pages/PluginDetails.test.tsx | 5 ----- 12 files changed, 8 insertions(+), 46 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 290d3040229..cc448ec4ba3 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -35,7 +35,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | Yes | | `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes | | `unifiedRequestLog` | Writes error logs to the request logger | Yes | -| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | Yes | | `recordedQueriesMulti` | Enables writing multiple items from a single query within Recorded Queries | Yes | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | Yes | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | Yes | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 15b138e4ff7..97f2ba25bcd 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -166,11 +166,6 @@ export interface FeatureToggles { */ pluginsFrontendSandbox?: boolean; /** - * Enables right panel for the plugins details page - * @default true - */ - pluginsDetailsRightPanel?: boolean; - /** * Enables previous SQL data source dataset dropdown behavior */ sqlDatasourceDatabaseSelection?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 4fdb2c87c27..8bce171f419 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -261,14 +261,6 @@ var ( Stage: FeatureStagePrivatePreview, Owner: grafanaPluginsPlatformSquad, }, - { - Name: "pluginsDetailsRightPanel", - Description: "Enables right panel for the plugins details page", - Stage: FeatureStageGeneralAvailability, - FrontendOnly: true, - Expression: "true", - Owner: grafanaPluginsPlatformSquad, - }, { Name: "sqlDatasourceDatabaseSelection", Description: "Enables previous SQL data source dataset dropdown behavior", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 531ac398257..778164b525b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -33,7 +33,6 @@ faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true pluginsFrontendSandbox,privatePreview,@grafana/plugins-platform-backend,false,false,false -pluginsDetailsRightPanel,GA,@grafana/plugins-platform-backend,false,false,true sqlDatasourceDatabaseSelection,preview,@grafana/oss-big-tent,false,false,true recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false logsExploreTableVisualisation,GA,@grafana/observability-logs,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 96930b299c9..810b1e641cb 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -143,10 +143,6 @@ const ( // Enables the plugins frontend sandbox FlagPluginsFrontendSandbox = "pluginsFrontendSandbox" - // FlagPluginsDetailsRightPanel - // Enables right panel for the plugins details page - FlagPluginsDetailsRightPanel = "pluginsDetailsRightPanel" - // FlagSqlDatasourceDatabaseSelection // Enables previous SQL data source dataset dropdown behavior FlagSqlDatasourceDatabaseSelection = "sqlDatasourceDatabaseSelection" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 08c49aac219..18fde824fc9 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2348,7 +2348,8 @@ "metadata": { "name": "pluginsDetailsRightPanel", "resourceVersion": "1753285129398", - "creationTimestamp": "2024-08-13T09:55:30Z" + "creationTimestamp": "2024-08-13T09:55:30Z", + "deletionTimestamp": "2025-07-25T08:56:36Z" }, "spec": { "description": "Enables right panel for the plugins details page", diff --git a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx index 9a83a03b688..0cfbf334fb6 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsBody.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsBody.tsx @@ -87,7 +87,7 @@ export function PluginDetailsBody({ plugin, queryParams, pageId, info, showDetai return ; } - if (pageId === PluginTabIds.PLUGINDETAILS && config.featureToggles.pluginsDetailsRightPanel && showDetails) { + if (pageId === PluginTabIds.PLUGINDETAILS && showDetails) { return (
diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx index 4a5a7b5c7bd..da4eef2f0d4 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.test.tsx @@ -106,14 +106,7 @@ describe('PluginDetailsPage', () => { expect(screen.getByText('Plugin not found')).toBeInTheDocument(); }); - it('should not show right panel when feature toggle is disabled', () => { - config.featureToggles.pluginsDetailsRightPanel = false; - render(); - expect(screen.queryByTestId('plugin-details-panel')).not.toBeInTheDocument(); - }); - - it('should show right panel when feature toggle is enabled and screen is wide', () => { - config.featureToggles.pluginsDetailsRightPanel = true; + it('should show right panel when screen is wide', () => { window.matchMedia = jest.fn().mockImplementation((query) => ({ matches: query !== '(max-width: 600px)', media: query, @@ -128,7 +121,6 @@ describe('PluginDetailsPage', () => { }); it('should show "Plugin details" tab when screen is narrow', () => { - config.featureToggles.pluginsDetailsRightPanel = true; window.matchMedia = jest.fn().mockImplementation((query) => ({ matches: query === '(max-width: 600px)', media: query, @@ -158,7 +150,6 @@ describe('PluginDetailsPage', () => { }); it('should not show last version in plugin details panel when plugin is core', () => { - config.featureToggles.pluginsDetailsRightPanel = true; window.matchMedia = jest.fn().mockImplementation((query) => ({ matches: query !== '(max-width: 600px)', media: query, diff --git a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx index d5a69de88ec..0e651a8e4bf 100644 --- a/public/app/features/plugins/admin/components/PluginDetailsPage.tsx +++ b/public/app/features/plugins/admin/components/PluginDetailsPage.tsx @@ -5,7 +5,6 @@ import { useMedia } from 'react-use'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { config } from '@grafana/runtime'; import { Alert, Box, Stack, TabContent, TextLink, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { AppNotificationSeverity } from 'app/types/appNotifications'; @@ -83,10 +82,8 @@ export function PluginDetailsPage({ ); } - const conditionalProps = !config.featureToggles.pluginsDetailsRightPanel ? { info: info } : {}; - return ( - + @@ -102,9 +99,7 @@ export function PluginDetailsPage({ /> - {!isNarrowScreen && config.featureToggles.pluginsDetailsRightPanel && ( - - )} + {!isNarrowScreen && } ); diff --git a/public/app/features/plugins/admin/components/PluginSubtitle.tsx b/public/app/features/plugins/admin/components/PluginSubtitle.tsx index 0a66d863938..6e725eb014f 100644 --- a/public/app/features/plugins/admin/components/PluginSubtitle.tsx +++ b/public/app/features/plugins/admin/components/PluginSubtitle.tsx @@ -2,7 +2,6 @@ import { css } from '@emotion/css'; import { Fragment } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { Alert, Stack, useStyles2 } from '@grafana/ui'; import { InstallControlsWarning } from '../components/InstallControls/InstallControlsWarning'; @@ -46,7 +45,7 @@ export const PluginSubtitle = ({ plugin }: Props) => {
{plugin?.description &&
{plugin?.description}
} - {!config.featureToggles.pluginsDetailsRightPanel && !!plugin?.details?.links?.length && ( + {!!plugin?.details?.links?.length && ( {plugin.details.links.map((link, index) => ( diff --git a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx index f0eebd3bd0a..6f0f076d54b 100644 --- a/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginDetailsTabs.tsx @@ -74,7 +74,7 @@ export const usePluginDetailsTabs = ( }); } - if (isPublished && isNarrowScreen && config.featureToggles.pluginsDetailsRightPanel) { + if (isPublished && isNarrowScreen) { navModelChildren.push({ text: PluginTabLabels.PLUGINDETAILS, id: PluginTabIds.PLUGINDETAILS, diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index b725d3a9d58..b7aaed5cef0 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -885,11 +885,6 @@ describe('Plugin details page', () => { isDataSourceEditor: false, isOrgAdmin: true, }); - config.featureToggles.pluginsDetailsRightPanel = true; - }); - - afterAll(() => { - config.featureToggles.pluginsDetailsRightPanel = false; }); it('should display Latest release date and report a concern information', async () => { From f67713726bd5943a80aa97eda81a2ab73b085987 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Fri, 25 Jul 2025 08:36:35 -0400 Subject: [PATCH 097/101] add debug logs to search server distributor (#108679) --- pkg/storage/unified/resource/search_server_distributor.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/storage/unified/resource/search_server_distributor.go b/pkg/storage/unified/resource/search_server_distributor.go index 3fc0203282d..14527f0e6b3 100644 --- a/pkg/storage/unified/resource/search_server_distributor.go +++ b/pkg/storage/unified/resource/search_server_distributor.go @@ -131,11 +131,13 @@ func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, n ringHasher := fnv.New32a() _, err := ringHasher.Write([]byte(namespace)) if err != nil { + ds.log.Debug("error hashing namespace", "err", err, "namespace", namespace) return ctx, nil, err } rs, err := ds.ring.GetWithOptions(ringHasher.Sum32(), searchRingRead, ring.WithReplicationFactor(ds.ring.ReplicationFactor())) if err != nil { + ds.log.Debug("error getting replication set from ring", "err", err, "namespace", namespace) return ctx, nil, err } @@ -143,6 +145,7 @@ func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, n inst := rs.Instances[rand.Intn(len(rs.Instances))] client, err := ds.clientPool.GetClientForInstance(inst) if err != nil { + ds.log.Debug("error getting instance client from pool", "err", err, "namespace", namespace, "searchApiInstanceId", inst.Id) return ctx, nil, err } @@ -153,7 +156,10 @@ func (ds *distributorServer) getClientToDistributeRequest(ctx context.Context, n ds.log.Info("distributing request to ", "methodName", methodName, "instanceId", inst.Id, "namespace", namespace) - _ = grpc.SetHeader(ctx, metadata.Pairs("proxied-instance-id", inst.Id)) + err = grpc.SetHeader(ctx, metadata.Pairs("proxied-instance-id", inst.Id)) + if err != nil { + ds.log.Debug("error setting grpc header", err, "err") + } return userutils.InjectOrgID(metadata.NewOutgoingContext(ctx, md), namespace), client.(*RingClient).Client, nil } From 9ad88814113db40559b1e6a677d60c3f6e63b426 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:38:05 +0100 Subject: [PATCH 098/101] Update dependency fork-ts-checker-webpack-plugin to v9.1.0 (#108672) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- packages/grafana-plugin-configs/package.json | 2 +- yarn.lock | 22 ++++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index fa37681c8ac..91440a05642 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "eslint-webpack-plugin": "4.2.0", "expose-loader": "5.0.1", "fishery": "^2.2.2", - "fork-ts-checker-webpack-plugin": "9.0.2", + "fork-ts-checker-webpack-plugin": "9.1.0", "glob": "11.0.3", "html-loader": "5.1.0", "html-webpack-plugin": "5.6.3", diff --git a/packages/grafana-plugin-configs/package.json b/packages/grafana-plugin-configs/package.json index b42fc2ce215..239138a9f4f 100644 --- a/packages/grafana-plugin-configs/package.json +++ b/packages/grafana-plugin-configs/package.json @@ -17,7 +17,7 @@ "copy-webpack-plugin": "12.0.2", "eslint": "9.31.0", "eslint-webpack-plugin": "4.2.0", - "fork-ts-checker-webpack-plugin": "9.0.2", + "fork-ts-checker-webpack-plugin": "9.1.0", "glob": "11.0.3", "identity-obj-proxy": "^3.0.0", "imports-loader": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 6b0cdd0997d..d256bcfcd6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3437,7 +3437,7 @@ __metadata: copy-webpack-plugin: "npm:12.0.2" eslint: "npm:9.31.0" eslint-webpack-plugin: "npm:4.2.0" - fork-ts-checker-webpack-plugin: "npm:9.0.2" + fork-ts-checker-webpack-plugin: "npm:9.1.0" glob: "npm:11.0.3" identity-obj-proxy: "npm:^3.0.0" imports-loader: "npm:^5.0.0" @@ -12842,12 +12842,12 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^4.0.0": - version: 4.0.1 - resolution: "chokidar@npm:4.0.1" +"chokidar@npm:^4.0.0, chokidar@npm:^4.0.1": + version: 4.0.3 + resolution: "chokidar@npm:4.0.3" dependencies: readdirp: "npm:^4.0.1" - checksum: 10/62749d2173a60cc5632d6c6e0b7024f33aadce47b06d02e55ad03c7b8daaaf2fc85d4296c047473d04387fd992dab9384cc5263c70a3dc3018b7ebecfb5b5217 + checksum: 10/bf2a575ea5596000e88f5db95461a9d59ad2047e939d5a4aac59dd472d126be8f1c1ff3c7654b477cf532d18f42a97279ef80ee847972fd2a25410bf00b80b59 languageName: node linkType: hard @@ -17262,13 +17262,13 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:9.0.2": - version: 9.0.2 - resolution: "fork-ts-checker-webpack-plugin@npm:9.0.2" +"fork-ts-checker-webpack-plugin@npm:9.1.0": + version: 9.1.0 + resolution: "fork-ts-checker-webpack-plugin@npm:9.1.0" dependencies: "@babel/code-frame": "npm:^7.16.7" chalk: "npm:^4.1.2" - chokidar: "npm:^3.5.3" + chokidar: "npm:^4.0.1" cosmiconfig: "npm:^8.2.0" deepmerge: "npm:^4.2.2" fs-extra: "npm:^10.0.0" @@ -17281,7 +17281,7 @@ __metadata: peerDependencies: typescript: ">3.6.0" webpack: ^5.11.0 - checksum: 10/3399ea114d9397da5e51baaa1cf2ca704f4616c8255df672889cafc2a823f77e8bf9913133852d29d676657b1d40e4a2fba8eb9fafee09e00faeda924289220e + checksum: 10/1d24387224f7d49a17f7e44c9150971172f34ae30c4b1f581b8af967e73e8f36a434ed56f78aa45fd8cf0833c73a1b020102cc61070d7dc630b70c21c9770a1b languageName: node linkType: hard @@ -18340,7 +18340,7 @@ __metadata: fast-json-patch: "npm:3.1.1" file-saver: "npm:2.0.5" fishery: "npm:^2.2.2" - fork-ts-checker-webpack-plugin: "npm:9.0.2" + fork-ts-checker-webpack-plugin: "npm:9.1.0" glob: "npm:11.0.3" history: "npm:4.10.1" html-loader: "npm:5.1.0" From fb08352300fc53120197cf07a970e2fe7e9ef0a4 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 25 Jul 2025 14:42:37 +0200 Subject: [PATCH 099/101] Alerting: Rename draft new rule option (#108597) * Rename draft new rule option * Update tests * Update translation key --- .../unified/rule-list/RuleList.v2.test.tsx | 14 +++++++------- .../alerting/unified/rule-list/RuleList.v2.tsx | 2 +- public/locales/en-US/grafana.json | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx index 1f2f4c0b02e..9417ffa2230 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.test.tsx @@ -155,11 +155,11 @@ describe('RuleList v2', () => { describe('RuleListActions', () => { const ui = { - newRuleButton: byRole('link', { name: /new alert rule/i }), + newRuleButton: byRole('link', { name: /^new alert rule$/i }), moreButton: byRole('button', { name: /more/i }), moreMenu: byRole('menu'), menuOptions: { - draftNewRule: byRole('link', { name: /draft a new rule/i }), + newAlertRuleForExport: byRole('link', { name: /new alert rule for export/i }), newGrafanaRecordingRule: byRole('link', { name: /new grafana recording rule/i }), newDataSourceRecordingRule: byRole('link', { name: /new data source recording rule/i }), }, @@ -191,7 +191,7 @@ describe('RuleListActions', () => { expect(ui.moreButton.get()).toBeInTheDocument(); }); - it('should only show Draft a new rule when the user has view Grafana rules permission', async () => { + it('should only show New alert rule for export when the user has view Grafana rules permission', async () => { grantUserPermissions([AccessControlAction.AlertingRuleRead]); const { user } = render(); @@ -200,7 +200,7 @@ describe('RuleListActions', () => { const menu = await ui.moreMenu.find(); expect(ui.newRuleButton.query()).not.toBeInTheDocument(); - expect(ui.menuOptions.draftNewRule.query(menu)).toBeInTheDocument(); + expect(ui.menuOptions.newAlertRuleForExport.query(menu)).toBeInTheDocument(); expect(ui.menuOptions.newGrafanaRecordingRule.query(menu)).not.toBeInTheDocument(); expect(ui.menuOptions.newDataSourceRecordingRule.query(menu)).not.toBeInTheDocument(); }); @@ -213,7 +213,7 @@ describe('RuleListActions', () => { await user.click(ui.moreButton.get()); const menu = await ui.moreMenu.find(); - expect(ui.menuOptions.draftNewRule.query(menu)).toBeInTheDocument(); + expect(ui.menuOptions.newAlertRuleForExport.query(menu)).toBeInTheDocument(); expect(ui.menuOptions.newGrafanaRecordingRule.query(menu)).toBeInTheDocument(); expect(ui.menuOptions.newDataSourceRecordingRule.query(menu)).not.toBeInTheDocument(); }); @@ -226,7 +226,7 @@ describe('RuleListActions', () => { await user.click(ui.moreButton.get()); const menu = await ui.moreMenu.find(); - expect(ui.menuOptions.draftNewRule.query(menu)).toBeInTheDocument(); + expect(ui.menuOptions.newAlertRuleForExport.query(menu)).toBeInTheDocument(); expect(ui.menuOptions.newGrafanaRecordingRule.query(menu)).not.toBeInTheDocument(); expect(ui.menuOptions.newDataSourceRecordingRule.query(menu)).toBeInTheDocument(); }); @@ -239,7 +239,7 @@ describe('RuleListActions', () => { await user.click(ui.moreButton.get()); const menu = await ui.moreMenu.find(); - expect(ui.menuOptions.draftNewRule.query(menu)).toBeInTheDocument(); + expect(ui.menuOptions.newAlertRuleForExport.query(menu)).toBeInTheDocument(); expect(ui.menuOptions.newGrafanaRecordingRule.query(menu)).toBeInTheDocument(); expect(ui.menuOptions.newDataSourceRecordingRule.query(menu)).toBeInTheDocument(); }); diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx index 017750b45b1..a677cb1d248 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx @@ -47,7 +47,7 @@ export function RuleListActions() { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 20f007d6edf..a6d6f11aeb0 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2468,7 +2468,6 @@ }, "rule-list": { "configure-datasource": "Configure", - "draft-new-rule": "Draft a new rule", "ds-error": { "title": "Cannot load rules for this datasource" }, @@ -2494,6 +2493,7 @@ "new-alert-rule": "New alert rule", "new-datasource-recording-rule": "New Data source recording rule", "new-grafana-recording-rule": "New Grafana recording rule", + "new-rule-for-export": "New alert rule for export", "pagination": { "next-page": "Show more…" }, @@ -3515,7 +3515,7 @@ "this-folder-is-empty": "This folder is empty" }, "bulk-action-resources-form": { - "failed-alert_one": "{{count}} item failed", + "failed-alert_one": "{{count}} items failed", "failed-alert_other": "{{count}} items failed" }, "bulk-delete-resources-form": { @@ -9256,9 +9256,9 @@ "type": { "loki": { "indexed-label_one": "Indexed label", - "indexed-label_other": "Indexed labels", + "indexed-label_other": "Indexed label", "parsedl-label_one": "Parsed field", - "parsedl-label_other": "Parsed fields", + "parsedl-label_other": "Parsed field", "structured-metadata_one": "Structured metadata", "structured-metadata_other": "Structured metadata" } @@ -10987,7 +10987,7 @@ }, "bootstrap-step": { "dashboards-count_one": "{{count}} dashboard", - "dashboards-count_other": "{{count}} dashboards", + "dashboards-count_other": "{{count}} dashboard", "description-clear-repository-connection": "Add a clear name for this repository connection", "empty": "Empty", "error-field-required": "This field is required.", @@ -10995,7 +10995,7 @@ "files-count_one": "{{count}} files", "files-count_other": "{{count}} files", "folders-count_one": "{{count}} folder", - "folders-count_other": "{{count}} folders", + "folders-count_other": "{{count}} folder", "grafana": "Grafana instance", "label-display-name": "Display name", "placeholder-my-repository-connection": "My repository connection", From 6af2e81cda921b677d06d2428b3b301eb100d731 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:59:53 +0100 Subject: [PATCH 100/101] DashboardEdit: Undo/redo for conditional rendering (#107235) * add undo/redo for group and base actions * add undo/redo for rules * adjust circular dependency issue * add itemType translations * adjust how we get rendering group --- .../ConditionalRendering.tsx | 4 +- .../ConditionalRenderingBase.tsx | 41 +++++++++++- .../ConditionalRenderingData.tsx | 13 +++- .../ConditionalRenderingGroup.tsx | 65 +++++++++++++++++-- .../ConditionalRenderingGroupAdd.tsx | 4 +- .../ConditionalRenderingGroupVisibility.tsx | 3 +- .../ConditionalRenderingTimeRangeSize.tsx | 23 +++++-- .../ConditionalRenderingVariable.tsx | 35 ++++++++-- .../conditional-rendering/utils.ts | 14 ++++ public/locales/en-US/grafana.json | 5 ++ 10 files changed, 184 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx index 4d55174d175..ca0183e0e37 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRendering.tsx @@ -7,7 +7,7 @@ import { ConditionalRenderingChangedEvent } from '../edit-pane/shared'; import { ConditionalRenderingBase } from './ConditionalRenderingBase'; import { ConditionalRenderingGroup } from './ConditionalRenderingGroup'; import { ItemsWithConditionalRendering } from './types'; -import { getItemType } from './utils'; +import { getItemType, translatedItemType } from './utils'; export interface ConditionalRenderingState extends SceneObjectState { rootGroup: ConditionalRenderingGroup; @@ -20,7 +20,7 @@ export class ConditionalRendering extends SceneObjectBase extends SceneObjectState { value: V; @@ -74,6 +82,22 @@ export abstract class ConditionalRenderingBase< this.notifyChange(); } + public findRule() { + return this.getRenderingGroup().getRule(this.state.key!); + } + + public undoDeleted(index: number, rule: ConditionalRenderingConditions) { + const group = this.getRenderingGroup(); + const restoredState = [...group.state.value]; + restoredState.splice(index, 0, rule); + group.setStateAndNotify({ value: restoredState }); + } + + private getRenderingGroup(): ConditionalRenderingGroup { + // TODO: Adjust once nested rules are introduced to get relevant ConditionalRenderingGroup + return this._getConditionalLogicRoot().state.rootGroup; + } + private _getConditionalLogicRoot(): ConditionalRendering { return sceneGraph.getAncestor(this, ConditionalRendering); } @@ -115,7 +139,20 @@ function ConditionalRenderingBaseRenderer({ model.onDelete()} + onClick={() => { + const { rule, ruleIndex } = model.findRule(); + + dashboardEditActions.edit({ + description: t('dashboard.conditional-rendering.conditions.shared.delete-condition', 'Delete Condition'), + source: model, + perform: () => model.onDelete(), + undo: () => { + if (rule) { + model.undoDeleted(ruleIndex, rule); + } + }, + }); + }} /> diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx index b881486cc01..b39090bb1df 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingData.tsx @@ -6,10 +6,12 @@ import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; import { ConditionalRenderingDataKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Combobox, ComboboxOption } from '@grafana/ui'; +import { dashboardEditActions } from '../edit-pane/shared'; import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem'; import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase'; import { ConditionalRenderingSerializerRegistryItem, DataConditionValue, ItemsWithConditionalRendering } from './types'; +import { translatedItemType } from './utils'; type ConditionalRenderingDataState = ConditionalRenderingBaseState; @@ -32,7 +34,7 @@ export class ConditionalRenderingData extends ConditionalRenderingBase model.setStateAndNotify({ value: Boolean(value) })} + onChange={({ value: val }) => { + dashboardEditActions.edit({ + description: t('dashboard.edit-actions.edit-query-result-rule', 'Change query result rule'), + source: model, + perform: () => model.setStateAndNotify({ value: Boolean(val) }), + undo: () => model.setStateAndNotify({ value }), + }); + }} /> ); } diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx index 0e18d5e5e0a..ee949871a8f 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroup.tsx @@ -1,8 +1,13 @@ +import { capitalize, lowerCase } from 'lodash'; +import { useMemo } from 'react'; + import { t } from '@grafana/i18n'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; import { ConditionalRenderingGroupKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Stack } from '@grafana/ui'; +import { dashboardEditActions } from '../edit-pane/shared'; + import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase'; import { ConditionalRenderingData } from './ConditionalRenderingData'; import { ConditionalRenderingGroupAdd } from './ConditionalRenderingGroupAdd'; @@ -18,7 +23,9 @@ import { GroupConditionItemType, GroupConditionVisibility, GroupConditionValue, + ConditionalRenderingConditions, } from './types'; +import { translatedItemType } from './utils'; export interface ConditionalRenderingGroupState extends ConditionalRenderingBaseState { visibility: GroupConditionVisibility; @@ -63,7 +70,7 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase condition.state.key !== key) }); } + public removeLastItem() { + const newValues = [...this.state.value]; + newValues.pop(); + this.setStateAndNotify({ value: newValues }); + } + + public getRule(key: string) { + const ruleIndex = this.state.value.findIndex((rule) => rule.state.key === key); + const rule = this.state.value[ruleIndex]; + return { rule, ruleIndex }; + } + public serialize(): ConditionalRenderingGroupKind { if (this.state.value.some((item) => item instanceof ConditionalRenderingGroup)) { throw new Error('ConditionalRenderingGroup cannot contain nested ConditionalRenderingGroups'); @@ -127,22 +150,52 @@ export class ConditionalRenderingGroup extends ConditionalRenderingBase) { const { condition, visibility, value } = model.useState(); const { variables } = sceneGraph.getVariables(model).useState(); + const itemType = useMemo(() => model.getItemType(), [model]); return ( model.changeVisibility(value)} + onChange={(value) => { + dashboardEditActions.edit({ + description: t('dashboard.conditional-rendering.conditions.group.visibility.label', '{{type}} visibility', { + type: capitalize(translatedItemType(itemType)), + }), + source: model, + perform: () => model.changeVisibility(value), + undo: () => model.changeVisibility(visibility), + }); + }} /> {value.length > 1 && ( - model.changeCondition(value)} /> + { + dashboardEditActions.edit({ + description: t('dashboard.conditional-rendering.conditions.group.condition.label', 'Match rules'), + source: model, + perform: () => model.changeCondition(value), + undo: () => model.changeCondition(condition), + }); + }} + /> )} {value.map((entry) => entry.render())} 0} - onAdd={(itemType) => model.addItem(itemType)} + onAdd={({ value, label }) => { + const item = model.createItem(value!); + dashboardEditActions.edit({ + description: t('dashboard.edit-actions.add-conditional-rule', 'Add {{ruleDescription}} rule', { + ruleDescription: lowerCase(label), + }), + source: model, + perform: () => model.addItem(item), + undo: () => model.removeLastItem(), + }); + }} /> ); diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupAdd.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupAdd.tsx index 110f7a0d14e..e423870f6c8 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupAdd.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupAdd.tsx @@ -9,7 +9,7 @@ import { GroupConditionItemType, ItemsWithConditionalRendering } from './types'; interface Props { itemType: ItemsWithConditionalRendering; hasVariables: boolean; - onAdd: (itemType: GroupConditionItemType) => void; + onAdd: (option: SelectableValue) => void; } export const ConditionalRenderingGroupAdd = ({ itemType, hasVariables, onAdd }: Props) => { @@ -42,7 +42,7 @@ export const ConditionalRenderingGroupAdd = ({ itemType, hasVariables, onAdd }: variant="secondary" label={t('dashboard.conditional-rendering.conditions.group.add.button', 'Add rule')} options={options} - onChange={({ value }) => onAdd(value!)} + onChange={(option) => onAdd(option)} /> ); }; diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupVisibility.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupVisibility.tsx index 3ed91b28c37..0c105d798c5 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupVisibility.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupVisibility.tsx @@ -7,6 +7,7 @@ import { t } from '@grafana/i18n'; import { Field, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { GroupConditionVisibility, ItemsWithConditionalRendering } from './types'; +import { translatedItemType } from './utils'; interface Props { itemType: ItemsWithConditionalRendering; @@ -28,7 +29,7 @@ export const ConditionalRenderingGroupVisibility = ({ itemType, value, onChange return ( diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx index 4e57cd08347..28033cf18bb 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingTimeRangeSize.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { rangeUtil, SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -7,8 +7,11 @@ import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; import { ConditionalRenderingTimeRangeSizeKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Field, Select, useStyles2 } from '@grafana/ui'; +import { dashboardEditActions } from '../edit-pane/shared'; + import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase'; import { ConditionalRenderingSerializerRegistryItem, TimeRangeSizeConditionValue } from './types'; +import { translatedItemType } from './utils'; type ConditionalRenderingTimeRangeSizeState = ConditionalRenderingBaseState; @@ -29,7 +32,7 @@ export class ConditionalRenderingTimeRangeSize extends ConditionalRenderingBase< return t( 'dashboard.conditional-rendering.conditions.time-range-size.info', 'Show or hide the {{type}} if the dashboard time range is shorter than the selected time frame.', - { type: this.getItemType() } + { type: translatedItemType(this.getItemType()) } ); } @@ -192,6 +195,18 @@ function ConditionalRenderingTimeRangeSizeRenderer({ model }: SceneComponentProp return [{ label: value, value }, ...staticOptions]; }, [staticOptions, value]); + const handleChange = useCallback( + (newValue: string | undefined) => { + dashboardEditActions.edit({ + description: t('dashboard.edit-actions.edit-time-range-rule', 'Change time range rule'), + source: model, + perform: () => model.setStateAndNotify({ value: newValue }), + undo: () => model.setStateAndNotify({ value }), + }); + }, + [model, value] + ); + return ( model.setStateAndNotify({ value })} + onCreateOption={(value) => handleChange(value)} value={value} options={options} - onChange={({ value }) => model.setStateAndNotify({ value })} + onChange={({ value }) => handleChange(value)} /> ); diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx index c0d1454ba8f..5045f76d885 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingVariable.tsx @@ -5,12 +5,15 @@ import { SceneComponentProps, sceneGraph, VariableDependencyConfig } from '@graf import { ConditionalRenderingVariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Box, Combobox, ComboboxOption, Input, Stack } from '@grafana/ui'; +import { dashboardEditActions } from '../edit-pane/shared'; + import { ConditionalRenderingBase, ConditionalRenderingBaseState } from './ConditionalRenderingBase'; import { ConditionalRenderingSerializerRegistryItem, VariableConditionValue, VariableConditionValueOperator, } from './types'; +import { translatedItemType } from './utils'; type ConditionalRenderingVariableState = ConditionalRenderingBaseState; @@ -31,7 +34,7 @@ export class ConditionalRenderingVariable extends ConditionalRenderingBase @@ -118,7 +123,14 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps model.setStateAndNotify({ value: { ...value, name: option.value } })} + onChange={(option) => { + dashboardEditActions.edit({ + description: undoText, + source: model, + perform: () => model.setStateAndNotify({ value: { ...value, name: option.value } }), + undo: () => model.setStateAndNotify({ value: { ...value, name: value.name } }), + }); + }} /> @@ -127,13 +139,28 @@ function ConditionalRenderingVariableRenderer({ model }: SceneComponentProps model.setStateAndNotify({ value: { ...value, operator: option.value } })} + onChange={(option) => { + dashboardEditActions.edit({ + description: undoText, + source: model, + perform: () => model.setStateAndNotify({ value: { ...value, operator: option.value } }), + undo: () => model.setStateAndNotify({ value: { ...value, operator: value.operator } }), + }); + }} /> model.setStateAndNotify({ value: { ...value, value: e.currentTarget.value } })} + onChange={(e) => { + const eventValue = e.currentTarget.value; + dashboardEditActions.edit({ + description: undoText, + source: model, + perform: () => model.setStateAndNotify({ value: { ...value, value: eventValue } }), + undo: () => model.setStateAndNotify({ value: { ...value, value: value.value } }), + }); + }} /> ); diff --git a/public/app/features/dashboard-scene/conditional-rendering/utils.ts b/public/app/features/dashboard-scene/conditional-rendering/utils.ts index 87c71e723b9..d8e1b841692 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/utils.ts +++ b/public/app/features/dashboard-scene/conditional-rendering/utils.ts @@ -1,3 +1,6 @@ +import { lowerCase } from 'lodash'; + +import { t } from '@grafana/i18n'; import { SceneObject } from '@grafana/scenes'; import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem'; @@ -17,3 +20,14 @@ export function getItemType(object: SceneObject): ItemsWithConditionalRendering return 'element'; } + +export const translatedItemType = (item: ItemsWithConditionalRendering) => { + const translations: { [key in ItemsWithConditionalRendering]: string } = { + panel: lowerCase(t('dashboard.edit-pane.elements.panel', 'Panel')), + row: lowerCase(t('dashboard.edit-pane.elements.row', 'Row')), + tab: lowerCase(t('dashboard.edit-pane.elements.tab', 'Tab')), + element: lowerCase(t('dashboard.edit-pane.elements.element', 'Element')), + }; + + return translations[item] || item; +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a6d6f11aeb0..cbe8fac0f9c 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4566,6 +4566,10 @@ }, "edit-actions": { "add": "Add {{typeName}}", + "add-conditional-rule": "Add {{ruleDescription}} rule", + "edit-query-result-rule": "Change query result rule", + "edit-template-variable-rule": "Change template variable rule", + "edit-time-range-rule": "Change time range rule", "move": "Move {{typeName}}", "panel-background": "Change panel background", "panel-description": "Change panel description", @@ -4580,6 +4584,7 @@ "edit-pane": { "elements": { "dashboard": "Dashboard", + "element": "Element", "local-variable": "Local variable", "multiple-elements": "Multiple elements", "multiple-elements-delete-text": "Are you sure you want to delete these elements?", From 97aa3cee8875aea2e029483d51fa8755d6e17918 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Fri, 25 Jul 2025 15:10:54 +0200 Subject: [PATCH 101/101] GHA: Run Go Workspace Checks conditionally on BE changes (#108655) --- .github/actions/change-detection/action.yml | 4 ++ .github/workflows/pr-go-workspace-check.yml | 61 +++++++++++++++++---- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/.github/actions/change-detection/action.yml b/.github/actions/change-detection/action.yml index b8864e985e2..e2c3a6c3b89 100644 --- a/.github/actions/change-detection/action.yml +++ b/.github/actions/change-detection/action.yml @@ -47,6 +47,10 @@ runs: - '.github/actions/checkout/**' - '**/go.mod' - '**/go.sum' + - 'go.mod' + - 'go.sum' + - 'go.work' + - 'go.work.sum' - '**.go' - 'pkg/**' - '!pkg/**.md' diff --git a/.github/workflows/pr-go-workspace-check.yml b/.github/workflows/pr-go-workspace-check.yml index f7b148ac83d..91dc3037f11 100644 --- a/.github/workflows/pr-go-workspace-check.yml +++ b/.github/workflows/pr-go-workspace-check.yml @@ -4,18 +4,31 @@ on: workflow_dispatch: pull_request: branches: [main] - paths: - - .github/workflows/pr-go-workspace-check.yml - - go.mod - - go.sum - - go.work - - go.work.sum - - '**/go.mod' - - '**/go.sum' - - '**.go' + +permissions: {} jobs: - check: + detect-changes: + name: Detect whether code changed + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + changed: ${{ steps.detect-changes.outputs.backend }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: true # required to get more history in the changed-files action + fetch-depth: 2 + - name: Detect changes + id: detect-changes + uses: ./.github/actions/change-detection + with: + self: .github/workflows/pr-go-workspace-check.yml + + check-workspace: + needs: detect-changes + if: needs.detect-changes.outputs.changed == 'true' name: Go Workspace Check runs-on: ubuntu-latest @@ -47,6 +60,8 @@ jobs: run: ./scripts/go-workspace/validate-dockerfile.sh check-wire: + needs: detect-changes + if: needs.detect-changes.outputs.changed == 'true' name: Check Wire Changes runs-on: ubuntu-latest @@ -114,3 +129,29 @@ jobs: fi exit 1 fi + + # This is the job that is actually required by rulesets. + # We want to only require one job instead of all the individual tests. + # Future work also allows us to start skipping some tests based on changed files. + required-go-workspace-check: + needs: + - check-workspace + - check-wire + # always() is the best function here. + # success() || failure() will skip this function if any need is also skipped. + # That means conditional jobs will fail the entire requirement check. + if: always() + + name: All Go Workspace Checks complete + runs-on: ubuntu-latest + steps: + - name: Check the checks + env: + NEEDS: ${{ toJson(needs) }} + run: | + FAILURES="$(echo "$NEEDS" | jq 'with_entries(select(.value.result == "failure")) | map_values(.result)')" + echo "$FAILURES" + if [ "$(echo "$FAILURES" | jq '. | length')" != "0" ]; then + exit 1 + fi + echo "All OK!"