From fe5ec40144ae7084fee29773bce52f32142cc74b Mon Sep 17 00:00:00 2001 From: Lorenzo Setale Date: Wed, 15 Apr 2026 14:50:46 +0200 Subject: [PATCH 1/6] feat(variables): deduplicate variables to avoid conflicts when multiple sources set them Signed-off-by: Lorenzo Setale --- src/components/output.tsx | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/components/output.tsx b/src/components/output.tsx index 429b004..a6049e2 100644 --- a/src/components/output.tsx +++ b/src/components/output.tsx @@ -1,7 +1,6 @@ import { DashboardBuilder, DashboardCursorSync, - DatasourceVariableBuilder, RowBuilder, TimePickerBuilder, } from '@grafana/grafana-foundation-sdk/dashboard'; @@ -28,29 +27,26 @@ export default function Component({ goBack, goForward, dashboardData }) { new TimePickerBuilder() .refreshIntervals(["1m", "5m", "15m", "30m", "1h", "6h", "1d", "7d"]) ) - .withVariable( - new DatasourceVariableBuilder("prometheus") - .label("Metrics Data source") - .type("prometheus") - .regex("(?!grafanacloud-usage|grafanacloud-ml-metrics).+") - .multi(false) - ) - .withVariable( - new DatasourceVariableBuilder("loki") - .label("Logs Data source") - .type("loki") - .regex("(?!grafanacloud.+usage-insights|grafanacloud.+alert-state-history).+") - .multi(false) - ); - - // Inject variables contributed by enabled features + + // Inject variables contributed by enabled features, deduplicating by name + // so that multiple features using the same datasource type (ex: prometheus, + // loki) don't produce duplicate variables in the output. + const seenVariables = new Map(); + // TODO: Mayyybe there is a better way for this to avoid dupes sources dashboardData.features.forEach((feat) => { if (!feat.enabled) return; feat.variables.forEach((variable) => { - dashboard = dashboard.withVariable(variable); + const built = variable.build(); + if (!seenVariables.has(built.name)) { + seenVariables.set(built.name, variable); + } }); }); + seenVariables.forEach((variable) => { + dashboard = dashboard.withVariable(variable); + }); + dashboard = dashboard.withRow(new RowBuilder("Overview")); dashboard = dashboard.withPanel( From 1a3f5e077d15971d0c7d5062366f7f8a68ddb14d Mon Sep 17 00:00:00 2001 From: Lorenzo Setale Date: Wed, 15 Apr 2026 14:52:13 +0200 Subject: [PATCH 2/6] feat(features): adds variables for loki and prom prom when needed Signed-off-by: Lorenzo Setale --- src/features/logs.tsx | 17 ++++++++++++++--- src/features/metrics.tsx | 21 ++++++++++++++++----- src/features/novus.tsx | 29 +++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/features/logs.tsx b/src/features/logs.tsx index 5d8e8c4..137b8de 100644 --- a/src/features/logs.tsx +++ b/src/features/logs.tsx @@ -1,10 +1,11 @@ import { useState } from 'react'; +import { BigValueGraphMode } from '@grafana/grafana-foundation-sdk/common'; import { DataqueryBuilder as LokiDataqueryBuilder } from '@grafana/grafana-foundation-sdk/loki'; -import { PanelBuilder as LogsPanelBuilder } from '@grafana/grafana-foundation-sdk/logs'; +import { DatasourceVariableBuilder } from "@grafana/grafana-foundation-sdk/dashboard"; import { LogsDedupStrategy } from '@grafana/grafana-foundation-sdk/common'; +import { PanelBuilder as LogsPanelBuilder } from '@grafana/grafana-foundation-sdk/logs'; import { PanelBuilder as StatsPanelBuilder } from '@grafana/grafana-foundation-sdk/stat'; -import { BigValueGraphMode } from '@grafana/grafana-foundation-sdk/common'; import { PanelBuilder as TextPanelBuilder, @@ -78,6 +79,16 @@ export function Component({ goBack, goForward, setDashboardPanels }){ ] }; + const genVariables = () => { + return [ + new DatasourceVariableBuilder("loki") + .label("Logs Data source") + .type("loki") + .regex("(?!grafanacloud.+usage-insights|grafanacloud.+alert-state-history).+") + .multi(false) + ]; + }; + const validate = () => { const newErrors = {}; if (formData.service_name.length == 0) { @@ -90,7 +101,7 @@ export function Component({ goBack, goForward, setDashboardPanels }){ const onSubmit = () => { if (!validate()) return; - setDashboardPanels(FeatureID, genOverviewPanels(), genPanels()); + setDashboardPanels(FeatureID, genOverviewPanels(), genPanels(), genVariables()); goForward(); } diff --git a/src/features/metrics.tsx b/src/features/metrics.tsx index 922331b..bbec7f7 100644 --- a/src/features/metrics.tsx +++ b/src/features/metrics.tsx @@ -1,9 +1,10 @@ -import { PanelBuilder as TimeSeriesPanelBuilder } from '@grafana/grafana-foundation-sdk/timeseries'; +import { useState } from 'react'; + import { DataqueryBuilder as PrometheusDataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus'; -import { VizLegendOptionsBuilder } from '@grafana/grafana-foundation-sdk/common'; +import { DatasourceVariableBuilder } from "@grafana/grafana-foundation-sdk/dashboard"; import { PanelBuilder as TextPanelBuilder, TextMode } from '@grafana/grafana-foundation-sdk/text'; - -import { useState } from 'react'; +import { PanelBuilder as TimeSeriesPanelBuilder } from '@grafana/grafana-foundation-sdk/timeseries'; +import { VizLegendOptionsBuilder } from '@grafana/grafana-foundation-sdk/common'; import { usePersistentState } from '../lib/usePersistentState.ts'; @@ -63,9 +64,19 @@ export function Component({ goBack, goForward, setDashboardPanels }){ return Object.keys(newErrors).length === 0 } + const genVariables = () => { + return [ + new DatasourceVariableBuilder("prometheus") + .label("Metrics Data source") + .type("prometheus") + .regex("(?!grafanacloud-usage|grafanacloud-ml-metrics).+") + .multi(false) + ]; + }; + const onSubmit = () => { if (!validate()) return; - setDashboardPanels(FeatureID, [], genPanels()); + setDashboardPanels(FeatureID, [], genPanels(), genVariables()); goForward(); } diff --git a/src/features/novus.tsx b/src/features/novus.tsx index cc3cb53..890999a 100644 --- a/src/features/novus.tsx +++ b/src/features/novus.tsx @@ -1,18 +1,18 @@ import { useState, useEffect, type ReactNode } from "react"; import { useAuth } from "react-oidc-context"; -import { useEnv } from "../components/env.tsx"; import { AutoComplete } from "primereact/autocomplete"; import { - PanelBuilder, - ConstantVariableBuilder, AdHocVariableBuilder, - VariableHide, + ConstantVariableBuilder, + DatasourceVariableBuilder, + FieldColorModeId, + MappingType, + PanelBuilder, ThresholdsConfigBuilder, ThresholdsMode, - MappingType, - FieldColorModeId, + VariableHide, } from "@grafana/grafana-foundation-sdk/dashboard"; import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus"; @@ -21,6 +21,7 @@ import { PanelBuilder as TextPanelBuilder, TextMode } from "@grafana/grafana-fou import { usePersistentState } from "../lib/usePersistentState.ts"; import { queryPrometheus } from "../lib/prometheusQuerier.ts"; +import { useEnv } from "../components/env.tsx"; export const FeatureID = "novus"; export const FeatureName = "Novus Runtime Information"; @@ -92,7 +93,23 @@ function buildVariables(runtime: string) { .build(); podFilter.filters = [{ key: "pod", operator: "!~", value: "novus-.*" }]; + const promDatasource = new DatasourceVariableBuilder("prometheus") + .label("Metrics Data source") + .hide(VariableHide.HideVariable) + .type("prometheus") + .regex(".+Novus.+") + .multi(false); + + const lokiDatasource = new DatasourceVariableBuilder("loki") + .label("Logs Data source") + .hide(VariableHide.HideVariable) + .type("loki") + .regex(".+Novus.+") + .multi(false) + return [ + promDatasource, + lokiDatasource, new ConstantVariableBuilder("namespace") .label("Novus Runtime / namespace") .value(runtime), From 6b077de970cf73895d08a2894fd3a74cf0d6d65b Mon Sep 17 00:00:00 2001 From: Lorenzo Setale Date: Wed, 15 Apr 2026 15:34:11 +0200 Subject: [PATCH 3/6] feat(output): set default loki and prometheus to some valeu if not present Signed-off-by: Lorenzo Setale --- src/components/output.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/components/output.tsx b/src/components/output.tsx index a6049e2..4fb62fe 100644 --- a/src/components/output.tsx +++ b/src/components/output.tsx @@ -1,6 +1,7 @@ import { DashboardBuilder, DashboardCursorSync, + DatasourceVariableBuilder, RowBuilder, TimePickerBuilder, } from '@grafana/grafana-foundation-sdk/dashboard'; @@ -12,6 +13,19 @@ import { import { useEnv } from '../components/env.tsx'; +const DEFAULT_VAR_LOKI_DATASOURCE = new DatasourceVariableBuilder("loki") + .label("Metrics Data source") + .type("loki") + .regex("(?!grafanacloud-usage|grafanacloud-ml-metrics).+") + .multi(false); + +const DEFAULT_VAR_PROMETHEUS_DATASOURCE = new DatasourceVariableBuilder("prometheus") + .label("Metrics Data source") + .type("prometheus") + .regex("(?!grafanacloud-usage|grafanacloud-ml-metrics).+") + .multi(false); + + export default function Component({ goBack, goForward, dashboardData }) { const env = useEnv(); const grafanaBaseurl = env?.["BUN_PUBLIC_GRAFANA_BASEURL"]; @@ -43,6 +57,16 @@ export default function Component({ goBack, goForward, dashboardData }) { }); }); + // Check and add loki and prometheus datasources if they were not set. + + if(!seenVariables.has("loki")){ + seenVariables.set("loki", DEFAULT_VAR_LOKI_DATASOURCE); + } + + if(!seenVariables.has("prometheus")){ + seenVariables.set("prometheus", DEFAULT_VAR_PROMETHEUS_DATASOURCE); + } + seenVariables.forEach((variable) => { dashboard = dashboard.withVariable(variable); }); From f0c73ff1b28ecf366041d940c30c73fc5b83843c Mon Sep 17 00:00:00 2001 From: Lorenzo Setale Date: Wed, 15 Apr 2026 15:34:43 +0200 Subject: [PATCH 4/6] refactor: removes variables when the default ones are good Signed-off-by: Lorenzo Setale --- src/features/logs.tsx | 13 +------------ src/features/metrics.tsx | 12 +----------- src/features/novus.tsx | 3 ++- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/features/logs.tsx b/src/features/logs.tsx index 137b8de..8ad8a2b 100644 --- a/src/features/logs.tsx +++ b/src/features/logs.tsx @@ -2,7 +2,6 @@ import { useState } from 'react'; import { BigValueGraphMode } from '@grafana/grafana-foundation-sdk/common'; import { DataqueryBuilder as LokiDataqueryBuilder } from '@grafana/grafana-foundation-sdk/loki'; -import { DatasourceVariableBuilder } from "@grafana/grafana-foundation-sdk/dashboard"; import { LogsDedupStrategy } from '@grafana/grafana-foundation-sdk/common'; import { PanelBuilder as LogsPanelBuilder } from '@grafana/grafana-foundation-sdk/logs'; import { PanelBuilder as StatsPanelBuilder } from '@grafana/grafana-foundation-sdk/stat'; @@ -79,16 +78,6 @@ export function Component({ goBack, goForward, setDashboardPanels }){ ] }; - const genVariables = () => { - return [ - new DatasourceVariableBuilder("loki") - .label("Logs Data source") - .type("loki") - .regex("(?!grafanacloud.+usage-insights|grafanacloud.+alert-state-history).+") - .multi(false) - ]; - }; - const validate = () => { const newErrors = {}; if (formData.service_name.length == 0) { @@ -101,7 +90,7 @@ export function Component({ goBack, goForward, setDashboardPanels }){ const onSubmit = () => { if (!validate()) return; - setDashboardPanels(FeatureID, genOverviewPanels(), genPanels(), genVariables()); + setDashboardPanels(FeatureID, genOverviewPanels(), genPanels()); goForward(); } diff --git a/src/features/metrics.tsx b/src/features/metrics.tsx index bbec7f7..5a84813 100644 --- a/src/features/metrics.tsx +++ b/src/features/metrics.tsx @@ -64,19 +64,9 @@ export function Component({ goBack, goForward, setDashboardPanels }){ return Object.keys(newErrors).length === 0 } - const genVariables = () => { - return [ - new DatasourceVariableBuilder("prometheus") - .label("Metrics Data source") - .type("prometheus") - .regex("(?!grafanacloud-usage|grafanacloud-ml-metrics).+") - .multi(false) - ]; - }; - const onSubmit = () => { if (!validate()) return; - setDashboardPanels(FeatureID, [], genPanels(), genVariables()); + setDashboardPanels(FeatureID, [], genPanels()); goForward(); } diff --git a/src/features/novus.tsx b/src/features/novus.tsx index 890999a..020aff6 100644 --- a/src/features/novus.tsx +++ b/src/features/novus.tsx @@ -93,13 +93,14 @@ function buildVariables(runtime: string) { .build(); podFilter.filters = [{ key: "pod", operator: "!~", value: "novus-.*" }]; + // We know that Novus users have data for prom and loki in Novus' datasources + // so we try to be smort and set them. const promDatasource = new DatasourceVariableBuilder("prometheus") .label("Metrics Data source") .hide(VariableHide.HideVariable) .type("prometheus") .regex(".+Novus.+") .multi(false); - const lokiDatasource = new DatasourceVariableBuilder("loki") .label("Logs Data source") .hide(VariableHide.HideVariable) From 7721f021036d4d828ba881a92e1ece876d9a5e24 Mon Sep 17 00:00:00 2001 From: Lorenzo Setale Date: Wed, 15 Apr 2026 15:35:33 +0200 Subject: [PATCH 5/6] fix(variables): loki now uses the right regex to remove grafana cloud insight and name is correct Signed-off-by: Lorenzo Setale --- src/components/output.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/output.tsx b/src/components/output.tsx index 4fb62fe..8b205b2 100644 --- a/src/components/output.tsx +++ b/src/components/output.tsx @@ -14,9 +14,9 @@ import { import { useEnv } from '../components/env.tsx'; const DEFAULT_VAR_LOKI_DATASOURCE = new DatasourceVariableBuilder("loki") - .label("Metrics Data source") + .label("Logs Data source") .type("loki") - .regex("(?!grafanacloud-usage|grafanacloud-ml-metrics).+") + .regex("(?!grafanacloud).+(?!-alert-state-history|-usage-insight)") .multi(false); const DEFAULT_VAR_PROMETHEUS_DATASOURCE = new DatasourceVariableBuilder("prometheus") From e46670deaea46587e52fa8cdb955cf3d66fd12ef Mon Sep 17 00:00:00 2001 From: Lorenzo Setale Date: Wed, 15 Apr 2026 16:40:58 +0200 Subject: [PATCH 6/6] refactor: removes unused import Signed-off-by: Lorenzo Setale --- src/features/metrics.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/metrics.tsx b/src/features/metrics.tsx index 5a84813..5616346 100644 --- a/src/features/metrics.tsx +++ b/src/features/metrics.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import { DataqueryBuilder as PrometheusDataqueryBuilder } from '@grafana/grafana-foundation-sdk/prometheus'; -import { DatasourceVariableBuilder } from "@grafana/grafana-foundation-sdk/dashboard"; import { PanelBuilder as TextPanelBuilder, TextMode } from '@grafana/grafana-foundation-sdk/text'; import { PanelBuilder as TimeSeriesPanelBuilder } from '@grafana/grafana-foundation-sdk/timeseries'; import { VizLegendOptionsBuilder } from '@grafana/grafana-foundation-sdk/common';