From 45762ef9a3779616d96c79b79a4880b69980078b Mon Sep 17 00:00:00 2001 From: Paul Farver Date: Sun, 19 Apr 2026 09:33:38 +0200 Subject: [PATCH 1/5] chore: clean up edge feature --- src/features/edge.tsx | 241 +++++++++++++++++++----------------------- 1 file changed, 109 insertions(+), 132 deletions(-) diff --git a/src/features/edge.tsx b/src/features/edge.tsx index 22977b7..97aee82 100644 --- a/src/features/edge.tsx +++ b/src/features/edge.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { queryPrometheus } from "../lib/prometheusQuerier.ts"; import { useAuth } from "react-oidc-context"; import { AutoComplete } from "primereact/autocomplete"; @@ -10,100 +10,74 @@ export const FeatureID = "edge"; export const FeatureName = "Edge Services"; export const FeatureIcon = "https://www.rabbitmq.com/assets/files/rabbitmq-logo-e91cacd38fcef5219149bc5cfa10b384.svg"; -type Rabbit = { +// ---- Types ---- + +interface Rabbit { rabbitmq_cluster: string; novus_region: string; -}; +} -type Queue = { +interface Queue { queue: string; -}; - -export function Component({ goBack, goForward, setDashboardPanels }) { +} - const auth = useAuth(); - const env = useEnv(); - const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; +interface Props { + goBack?: () => void; + goForward?: () => void; + setDashboardPanels: (id: string, overview: any[], panels: any[]) => void; +} - const [rabbits, setRabbits] = useState(); - const [selectedRegion, setSelectedRegion] = useState(); - const [queues, setQueues] = useState(); - const [queueFilter, setQueueFilter] = useState(".*"); - const [filteredQueues, setFilteredQueues] = useState(); +// ---- Data hook ---- - const [selectedRabbitCluster, setSelectedRabbitCluster] = useState(); - const [filteredRabbitClusters, setFilteredRabbitClusters] = useState< - Rabbit[] - >([]); +function useEdgeData( + host: string | undefined, + token: string | undefined, + isAuthenticated: boolean, + selectedRabbitCluster: Rabbit | undefined, +) { + const [rabbits, setRabbits] = useState([]); + const [queues, setQueues] = useState([]); - const getRabbitClusters = () => { - return queryPrometheus( + useEffect(() => { + if (!isAuthenticated) return; + queryPrometheus( host, "group(rabbitmq_identity_info{}) by (rabbitmq_cluster, novus_region)", - auth?.user?.id_token, - ).then((result) => { - return result.data.result.map((item: any) => { - return { + token, + ).then((result) => + setRabbits( + result.data.result.map((item: any) => ({ rabbitmq_cluster: item.metric.rabbitmq_cluster, novus_region: item.metric.novus_region, - }; - }); - }); - }; + })), + ), + ); + }, [isAuthenticated]); - const getRabbitQueues = () => { - let rabbitmq_cluster = selectedRabbitCluster?.rabbitmq_cluster; - if (rabbitmq_cluster === undefined) { + useEffect(() => { + if (!isAuthenticated || !selectedRabbitCluster) { setQueues([]); + return; } - return queryPrometheus( + queryPrometheus( host, - `group(rabbitmq_detailed_queue_messages{service="${rabbitmq_cluster}"}) by (queue)`, - auth?.user?.id_token, - ).then((result) => { - return result.data.result.map((item: any) => { - return { - queue: item.metric.queue, - }; - }); - }); - }; - - const searchRabbitClusters = (event) => { - let _filtered: Rabbit[]; - if (!event.query.trim().length) { - _filtered = rabbits ?? []; - } else { - let terms = event.query.trim().split(/[-_\s]/); - _filtered = - rabbits?.filter((r) => { - return terms.every( - (t) => - r.rabbitmq_cluster.toLowerCase().includes(t) || - r.novus_region.toLowerCase().includes(t), - ); - }) ?? []; - } - - setFilteredRabbitClusters(_filtered); - }; - - useEffect(() => { - if (!auth?.isAuthenticated) return; - getRabbitClusters().then((res) => setRabbits(res)); - }, [auth?.isAuthenticated]); + `group(rabbitmq_detailed_queue_messages{service="${selectedRabbitCluster.rabbitmq_cluster}"}) by (queue)`, + token, + ).then((result) => + setQueues( + result.data.result.map((item: any) => ({ queue: item.metric.queue })), + ), + ); + }, [isAuthenticated, selectedRabbitCluster]); - useEffect(() => { - if (!auth?.isAuthenticated || !selectedRabbitCluster) return; - getRabbitQueues().then((res) => setQueues(res)); - }, [auth?.isAuthenticated, selectedRabbitCluster]); + return { rabbits, queues }; +} - useEffect(() => { - setFilteredQueues(queues?.filter((q) => q.queue.match(queueFilter))); - }, [queues, queueFilter]); +// ---- Panel builders (pure, no React) ---- - const genRabbitOverview = () => { - return [new TimeSeriesPanelBuilder() +function buildPanels(cluster: string, queueFilter: string) { + return [ + new TimeSeriesPanelBuilder() .title("Queues") .height(8) .span(8) @@ -111,35 +85,65 @@ export function Component({ goBack, goForward, setDashboardPanels }) { new PrometheusDataqueryBuilder() .datasource({ uid: "$prometheus" }) .expr( - `sum(rabbitmq_detailed_queue_messages{service="${selectedRabbitCluster?.rabbitmq_cluster}", queue=~"${queueFilter}"}) by (queue)`, - ).legendFormat(`{{ queue }}`), - )]; - }; + `sum(rabbitmq_detailed_queue_messages{service="${cluster}", queue=~"${queueFilter}"}) by (queue)`, + ) + .legendFormat(`{{ queue }}`), + ), + ]; +} - const onSubmit = () => { - setDashboardPanels(FeatureID, [], genRabbitOverview()); - goForward(); - }; +// ---- Component ---- - const rabbitTemplate = (rabbit) => { - return ( -
- [{rabbit.novus_region.split("-")[0].toUpperCase()}]{" "} - {rabbit.rabbitmq_cluster} -
- ); - }; +export function Component({ goBack, goForward, setDashboardPanels }: Props) { + const auth = useAuth(); + const env = useEnv(); + const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; + const token = auth?.user?.id_token; + + const [selectedRabbitCluster, setSelectedRabbitCluster] = useState(); + const [filteredRabbitClusters, setFilteredRabbitClusters] = useState([]); + const [queueFilter, setQueueFilter] = useState(".*"); + + const { rabbits, queues } = useEdgeData(host, token, !!auth?.isAuthenticated, selectedRabbitCluster); + + const filteredQueues = queues.filter((q) => q.queue.match(queueFilter)); + + const searchRabbitClusters = useCallback( + (event: { query: string }) => { + if (!event.query.trim()) { + setFilteredRabbitClusters(rabbits); + return; + } + const terms = event.query.trim().split(/[-_\s]/); + setFilteredRabbitClusters( + rabbits.filter((r) => + terms.every( + (t) => + r.rabbitmq_cluster.toLowerCase().includes(t) || + r.novus_region.toLowerCase().includes(t), + ), + ), + ); + }, + [rabbits], + ); - const selectedRabbitTemplate = (rabbit) => { - return rabbit.rabbitmq_cluster; + const rabbitTemplate = (rabbit: Rabbit) => ( +
+ [{rabbit.novus_region.split("-")[0].toUpperCase()}] {rabbit.rabbitmq_cluster} +
+ ); + + const onSubmit = () => { + if (!selectedRabbitCluster) return; + setDashboardPanels(FeatureID, [], buildPanels(selectedRabbitCluster.rabbitmq_cluster, queueFilter)); + goForward?.(); }; return ( <>
-

- Edge Configuration -

+

Edge Configuration

Edge is our stateful services platform. This feature adds pre-built panels scoped to edge service instances. @@ -151,17 +155,11 @@ export function Component({ goBack, goForward, setDashboardPanels }) { value={selectedRabbitCluster} suggestions={filteredRabbitClusters} completeMethod={searchRabbitClusters} - onChange={(e) => { - if (e?.value?.rabbitmq_cluster != "") { - setSelectedRabbitCluster(e.value); - } else { - setSelectedRabbitCluster(undefined); - } - }} + onChange={(e) => setSelectedRabbitCluster(e.value?.rabbitmq_cluster ? e.value : undefined)} itemTemplate={rabbitTemplate} - selectedItemTemplate={selectedRabbitTemplate} + selectedItemTemplate={(rabbit: Rabbit) => rabbit.rabbitmq_cluster} dropdown - > + />

{selectedRabbitCluster && (
@@ -171,40 +169,19 @@ export function Component({ goBack, goForward, setDashboardPanels }) { className="form-input" value={queueFilter} onChange={(e) => setQueueFilter(e.target.value)} - > -
queues: {filteredQueues?.length}
+ /> +
queues: {filteredQueues.length}
- {filteredQueues?.map((q) => { - return
{q.queue}
; - })} + {filteredQueues.map((q) => ( +
{q.queue}
+ ))}
)} - {/*
- -
    - {rabbits - ?.filter((rabbit) => rabbit.novus_region === selectedRegion) - .map((rabbit) => ( -
  • {rabbit.rabbitmq_cluster}
  • - ))} -
-
*/}
{goBack && ( From 4eefa1bae81ed4dedf3a7d1166ac544e9fa77bad Mon Sep 17 00:00:00 2001 From: Paul Farver Date: Sun, 19 Apr 2026 10:18:49 +0200 Subject: [PATCH 2/5] feat: WIP valkey section in edge feature --- src/features/edge.tsx | 53 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/features/edge.tsx b/src/features/edge.tsx index 97aee82..742a2c6 100644 --- a/src/features/edge.tsx +++ b/src/features/edge.tsx @@ -4,7 +4,7 @@ import { useAuth } from "react-oidc-context"; import { AutoComplete } from "primereact/autocomplete"; import { PanelBuilder as TimeSeriesPanelBuilder } from "@grafana/grafana-foundation-sdk/timeseries"; import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus"; -import { useEnv } from '../components/env.tsx'; +import { useEnv } from "../components/env.tsx"; export const FeatureID = "edge"; export const FeatureName = "Edge Services"; @@ -17,6 +17,11 @@ interface Rabbit { novus_region: string; } +interface Valkey { + valkey_cluster: string; + novus_region: string; +} + interface Queue { queue: string; } @@ -29,6 +34,8 @@ interface Props { // ---- Data hook ---- +const valkeyRegex = /(.*?)(?:-(?:valkey-)?metrics$)/; + function useEdgeData( host: string | undefined, token: string | undefined, @@ -37,6 +44,7 @@ function useEdgeData( ) { const [rabbits, setRabbits] = useState([]); const [queues, setQueues] = useState([]); + const [valkeys, setValkeys] = useState([]); useEffect(() => { if (!isAuthenticated) return; @@ -54,6 +62,24 @@ function useEdgeData( ); }, [isAuthenticated]); + useEffect(() => { + if (!isAuthenticated) return; + queryPrometheus( + host, + "group(redis_up{}) by (service, novus_region)", + token, + ).then((result) => + setValkeys( + result.data.result.map((item: any) => ({ + valkey_cluster: + item.metric.service.matchAll(valkeyRegex)?.[1] ?? + item.metric.service, + novus_region: item.metric.novus_region, + })), + ), + ); + }, [isAuthenticated]); + useEffect(() => { if (!isAuthenticated || !selectedRabbitCluster) { setQueues([]); @@ -70,7 +96,7 @@ function useEdgeData( ); }, [isAuthenticated, selectedRabbitCluster]); - return { rabbits, queues }; + return { rabbits, valkeys, queues }; } // ---- Panel builders (pure, no React) ---- @@ -104,7 +130,7 @@ export function Component({ goBack, goForward, setDashboardPanels }: Props) { const [filteredRabbitClusters, setFilteredRabbitClusters] = useState([]); const [queueFilter, setQueueFilter] = useState(".*"); - const { rabbits, queues } = useEdgeData(host, token, !!auth?.isAuthenticated, selectedRabbitCluster); + const { rabbits, valkeys, queues } = useEdgeData(host, token, !!auth?.isAuthenticated, selectedRabbitCluster); const filteredQueues = queues.filter((q) => q.queue.match(queueFilter)); @@ -130,10 +156,16 @@ export function Component({ goBack, goForward, setDashboardPanels }: Props) { const rabbitTemplate = (rabbit: Rabbit) => (
- [{rabbit.novus_region.split("-")[0].toUpperCase()}] {rabbit.rabbitmq_cluster} + [{rabbit.novus_region.split("-")[0]?.toUpperCase()}] {rabbit.rabbitmq_cluster}
); + const valkeyTemplate = (valkey: Valkey) => ( +
+ [{valkey.novus_region.split("-")[0]?.toUpperCase()}] {valkey.valkey_cluster} +
+ ) + const onSubmit = () => { if (!selectedRabbitCluster) return; setDashboardPanels(FeatureID, [], buildPanels(selectedRabbitCluster.rabbitmq_cluster, queueFilter)); @@ -181,6 +213,19 @@ export function Component({ goBack, goForward, setDashboardPanels }: Props) {
)} +
+ + + field="valkey_cluster" + value={selectedValkeyCluster} + suggestions={filteredValkeyClusters} + completeMethod={searchValkeyClusters} + onChange={(e) => setSelectedValkeyCluster(e.value?.valkey_cluster ? e.value : undefined)} + itemTemplate={valkeyTemplate} + selectedItemTemplate={(valkey: Valkey) => valkey.valkey_cluster} + dropdown + /> +
From 527ef5b0be5700b3abece4b5d6b9ca660dbd1f43 Mon Sep 17 00:00:00 2001 From: Paul Farver Date: Sun, 19 Apr 2026 10:36:29 +0200 Subject: [PATCH 3/5] feat: Build out valkey feature --- src/features/edge.tsx | 77 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/src/features/edge.tsx b/src/features/edge.tsx index 742a2c6..4612d1e 100644 --- a/src/features/edge.tsx +++ b/src/features/edge.tsx @@ -34,7 +34,7 @@ interface Props { // ---- Data hook ---- -const valkeyRegex = /(.*?)(?:-(?:valkey-)?metrics$)/; +const valkeyRegex = /^(.*?)(?:-(?:valkey-)?metrics)$/; function useEdgeData( host: string | undefined, @@ -72,7 +72,7 @@ function useEdgeData( setValkeys( result.data.result.map((item: any) => ({ valkey_cluster: - item.metric.service.matchAll(valkeyRegex)?.[1] ?? + item.metric.service.match(valkeyRegex)?.[1] ?? item.metric.service, novus_region: item.metric.novus_region, })), @@ -101,7 +101,7 @@ function useEdgeData( // ---- Panel builders (pure, no React) ---- -function buildPanels(cluster: string, queueFilter: string) { +function buildRabbitPanels(cluster: string, queueFilter: string) { return [ new TimeSeriesPanelBuilder() .title("Queues") @@ -118,6 +118,31 @@ function buildPanels(cluster: string, queueFilter: string) { ]; } +function buildValkeyPanels(cluster: string) { + return [ + new TimeSeriesPanelBuilder() + .title(`${cluster}: Commands`) + .height(8) + .span(12) + .withTarget( + new PrometheusDataqueryBuilder() + .datasource({ uid: "$prometheus" }) + .expr(`sum(rate(redis_commands_total{service=~"${cluster}(-valkey)?-metrics"}[$__rate_interval])) by (cmd)`) + .legendFormat(`{{ cmd }}`), + ), + new TimeSeriesPanelBuilder() + .title(`${cluster}: Memory`) + .height(8) + .span(12) + .withTarget( + new PrometheusDataqueryBuilder() + .datasource({ uid: "$prometheus" }) + .expr(`redis_memory_used_bytes{service=~"${cluster}(-valkey)?-metrics"}`) + .legendFormat(`{{ instance }}`), + ), + ]; +} + // ---- Component ---- export function Component({ goBack, goForward, setDashboardPanels }: Props) { @@ -126,13 +151,18 @@ export function Component({ goBack, goForward, setDashboardPanels }: Props) { const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; const token = auth?.user?.id_token; - const [selectedRabbitCluster, setSelectedRabbitCluster] = useState(); + const [rabbitValue, setRabbitValue] = useState(); const [filteredRabbitClusters, setFilteredRabbitClusters] = useState([]); const [queueFilter, setQueueFilter] = useState(".*"); + const [valkeyValue, setValkeyValue] = useState(); + const [filteredValkeyClusters, setFilteredValkeyClusters] = useState([]); + + const selectedRabbitCluster = typeof rabbitValue === "object" ? rabbitValue : undefined; + const selectedValkeyCluster = typeof valkeyValue === "object" ? valkeyValue : undefined; const { rabbits, valkeys, queues } = useEdgeData(host, token, !!auth?.isAuthenticated, selectedRabbitCluster); - const filteredQueues = queues.filter((q) => q.queue.match(queueFilter)); + const filteredQueues = queues.filter((q) => q.queue.match(`^${queueFilter}$`)); const searchRabbitClusters = useCallback( (event: { query: string }) => { @@ -160,15 +190,38 @@ export function Component({ goBack, goForward, setDashboardPanels }: Props) {
); + const searchValkeyClusters = useCallback( + (event: { query: string }) => { + if (!event.query.trim()) { + setFilteredValkeyClusters(valkeys); + return; + } + const terms = event.query.trim().split(/[-_\s]/); + setFilteredValkeyClusters( + valkeys.filter((v) => + terms.every( + (t) => + v.valkey_cluster.toLowerCase().includes(t) || + v.novus_region.toLowerCase().includes(t), + ), + ), + ); + }, + [valkeys], + ); + const valkeyTemplate = (valkey: Valkey) => (
[{valkey.novus_region.split("-")[0]?.toUpperCase()}] {valkey.valkey_cluster}
- ) + ); const onSubmit = () => { - if (!selectedRabbitCluster) return; - setDashboardPanels(FeatureID, [], buildPanels(selectedRabbitCluster.rabbitmq_cluster, queueFilter)); + const panels = [ + ...(selectedRabbitCluster ? buildRabbitPanels(selectedRabbitCluster.rabbitmq_cluster, queueFilter) : []), + ...(selectedValkeyCluster ? buildValkeyPanels(selectedValkeyCluster.valkey_cluster) : []), + ]; + setDashboardPanels(FeatureID, [], panels); goForward?.(); }; @@ -184,10 +237,10 @@ export function Component({ goBack, goForward, setDashboardPanels }: Props) { field="rabbitmq_cluster" - value={selectedRabbitCluster} + value={rabbitValue} suggestions={filteredRabbitClusters} completeMethod={searchRabbitClusters} - onChange={(e) => setSelectedRabbitCluster(e.value?.rabbitmq_cluster ? e.value : undefined)} + onChange={(e) => setRabbitValue(e.value)} itemTemplate={rabbitTemplate} selectedItemTemplate={(rabbit: Rabbit) => rabbit.rabbitmq_cluster} dropdown @@ -217,10 +270,10 @@ export function Component({ goBack, goForward, setDashboardPanels }: Props) { field="valkey_cluster" - value={selectedValkeyCluster} + value={valkeyValue} suggestions={filteredValkeyClusters} completeMethod={searchValkeyClusters} - onChange={(e) => setSelectedValkeyCluster(e.value?.valkey_cluster ? e.value : undefined)} + onChange={(e) => setValkeyValue(e.value)} itemTemplate={valkeyTemplate} selectedItemTemplate={(valkey: Valkey) => valkey.valkey_cluster} dropdown From dc56b6d00c5b0eaaf5537b3631c21a2ffc02aa81 Mon Sep 17 00:00:00 2001 From: Paul Farver Date: Sun, 19 Apr 2026 10:51:43 +0200 Subject: [PATCH 4/5] feat: Split edge feature into rabbitmq and valkey --- src/Wizard.tsx | 6 +- src/features/edge.tsx | 298 -------------------------------------- src/features/rabbitmq.tsx | 202 ++++++++++++++++++++++++++ src/features/valkey.tsx | 169 +++++++++++++++++++++ 4 files changed, 375 insertions(+), 300 deletions(-) delete mode 100644 src/features/edge.tsx create mode 100644 src/features/rabbitmq.tsx create mode 100644 src/features/valkey.tsx diff --git a/src/Wizard.tsx b/src/Wizard.tsx index f193cc7..1d7292f 100644 --- a/src/Wizard.tsx +++ b/src/Wizard.tsx @@ -9,7 +9,8 @@ import * as FeatureSLO from "./features/slo.tsx"; import * as FeatureLogs from "./features/logs.tsx"; import * as FeatureMetrics from "./features/metrics.tsx"; import * as FeatureNovus from "./features/novus.tsx"; -import * as FeatureEdge from "./features/edge.tsx"; +import * as FeatureRabbitMQ from "./features/rabbitmq.tsx"; +import * as FeatureValkey from "./features/valkey.tsx"; // Hard coded list of features that the users can tunr on/off const FEATURES = [ @@ -17,7 +18,8 @@ const FEATURES = [ FeatureSLO, FeatureMetrics, FeatureLogs, - FeatureEdge, + FeatureRabbitMQ, + FeatureValkey, ]; const DEFAULT_STEPS = [ diff --git a/src/features/edge.tsx b/src/features/edge.tsx deleted file mode 100644 index 4612d1e..0000000 --- a/src/features/edge.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { queryPrometheus } from "../lib/prometheusQuerier.ts"; -import { useAuth } from "react-oidc-context"; -import { AutoComplete } from "primereact/autocomplete"; -import { PanelBuilder as TimeSeriesPanelBuilder } from "@grafana/grafana-foundation-sdk/timeseries"; -import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus"; -import { useEnv } from "../components/env.tsx"; - -export const FeatureID = "edge"; -export const FeatureName = "Edge Services"; -export const FeatureIcon = "https://www.rabbitmq.com/assets/files/rabbitmq-logo-e91cacd38fcef5219149bc5cfa10b384.svg"; - -// ---- Types ---- - -interface Rabbit { - rabbitmq_cluster: string; - novus_region: string; -} - -interface Valkey { - valkey_cluster: string; - novus_region: string; -} - -interface Queue { - queue: string; -} - -interface Props { - goBack?: () => void; - goForward?: () => void; - setDashboardPanels: (id: string, overview: any[], panels: any[]) => void; -} - -// ---- Data hook ---- - -const valkeyRegex = /^(.*?)(?:-(?:valkey-)?metrics)$/; - -function useEdgeData( - host: string | undefined, - token: string | undefined, - isAuthenticated: boolean, - selectedRabbitCluster: Rabbit | undefined, -) { - const [rabbits, setRabbits] = useState([]); - const [queues, setQueues] = useState([]); - const [valkeys, setValkeys] = useState([]); - - useEffect(() => { - if (!isAuthenticated) return; - queryPrometheus( - host, - "group(rabbitmq_identity_info{}) by (rabbitmq_cluster, novus_region)", - token, - ).then((result) => - setRabbits( - result.data.result.map((item: any) => ({ - rabbitmq_cluster: item.metric.rabbitmq_cluster, - novus_region: item.metric.novus_region, - })), - ), - ); - }, [isAuthenticated]); - - useEffect(() => { - if (!isAuthenticated) return; - queryPrometheus( - host, - "group(redis_up{}) by (service, novus_region)", - token, - ).then((result) => - setValkeys( - result.data.result.map((item: any) => ({ - valkey_cluster: - item.metric.service.match(valkeyRegex)?.[1] ?? - item.metric.service, - novus_region: item.metric.novus_region, - })), - ), - ); - }, [isAuthenticated]); - - useEffect(() => { - if (!isAuthenticated || !selectedRabbitCluster) { - setQueues([]); - return; - } - queryPrometheus( - host, - `group(rabbitmq_detailed_queue_messages{service="${selectedRabbitCluster.rabbitmq_cluster}"}) by (queue)`, - token, - ).then((result) => - setQueues( - result.data.result.map((item: any) => ({ queue: item.metric.queue })), - ), - ); - }, [isAuthenticated, selectedRabbitCluster]); - - return { rabbits, valkeys, queues }; -} - -// ---- Panel builders (pure, no React) ---- - -function buildRabbitPanels(cluster: string, queueFilter: string) { - return [ - new TimeSeriesPanelBuilder() - .title("Queues") - .height(8) - .span(8) - .withTarget( - new PrometheusDataqueryBuilder() - .datasource({ uid: "$prometheus" }) - .expr( - `sum(rabbitmq_detailed_queue_messages{service="${cluster}", queue=~"${queueFilter}"}) by (queue)`, - ) - .legendFormat(`{{ queue }}`), - ), - ]; -} - -function buildValkeyPanels(cluster: string) { - return [ - new TimeSeriesPanelBuilder() - .title(`${cluster}: Commands`) - .height(8) - .span(12) - .withTarget( - new PrometheusDataqueryBuilder() - .datasource({ uid: "$prometheus" }) - .expr(`sum(rate(redis_commands_total{service=~"${cluster}(-valkey)?-metrics"}[$__rate_interval])) by (cmd)`) - .legendFormat(`{{ cmd }}`), - ), - new TimeSeriesPanelBuilder() - .title(`${cluster}: Memory`) - .height(8) - .span(12) - .withTarget( - new PrometheusDataqueryBuilder() - .datasource({ uid: "$prometheus" }) - .expr(`redis_memory_used_bytes{service=~"${cluster}(-valkey)?-metrics"}`) - .legendFormat(`{{ instance }}`), - ), - ]; -} - -// ---- Component ---- - -export function Component({ goBack, goForward, setDashboardPanels }: Props) { - const auth = useAuth(); - const env = useEnv(); - const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; - const token = auth?.user?.id_token; - - const [rabbitValue, setRabbitValue] = useState(); - const [filteredRabbitClusters, setFilteredRabbitClusters] = useState([]); - const [queueFilter, setQueueFilter] = useState(".*"); - const [valkeyValue, setValkeyValue] = useState(); - const [filteredValkeyClusters, setFilteredValkeyClusters] = useState([]); - - const selectedRabbitCluster = typeof rabbitValue === "object" ? rabbitValue : undefined; - const selectedValkeyCluster = typeof valkeyValue === "object" ? valkeyValue : undefined; - - const { rabbits, valkeys, queues } = useEdgeData(host, token, !!auth?.isAuthenticated, selectedRabbitCluster); - - const filteredQueues = queues.filter((q) => q.queue.match(`^${queueFilter}$`)); - - const searchRabbitClusters = useCallback( - (event: { query: string }) => { - if (!event.query.trim()) { - setFilteredRabbitClusters(rabbits); - return; - } - const terms = event.query.trim().split(/[-_\s]/); - setFilteredRabbitClusters( - rabbits.filter((r) => - terms.every( - (t) => - r.rabbitmq_cluster.toLowerCase().includes(t) || - r.novus_region.toLowerCase().includes(t), - ), - ), - ); - }, - [rabbits], - ); - - const rabbitTemplate = (rabbit: Rabbit) => ( -
- [{rabbit.novus_region.split("-")[0]?.toUpperCase()}] {rabbit.rabbitmq_cluster} -
- ); - - const searchValkeyClusters = useCallback( - (event: { query: string }) => { - if (!event.query.trim()) { - setFilteredValkeyClusters(valkeys); - return; - } - const terms = event.query.trim().split(/[-_\s]/); - setFilteredValkeyClusters( - valkeys.filter((v) => - terms.every( - (t) => - v.valkey_cluster.toLowerCase().includes(t) || - v.novus_region.toLowerCase().includes(t), - ), - ), - ); - }, - [valkeys], - ); - - const valkeyTemplate = (valkey: Valkey) => ( -
- [{valkey.novus_region.split("-")[0]?.toUpperCase()}] {valkey.valkey_cluster} -
- ); - - const onSubmit = () => { - const panels = [ - ...(selectedRabbitCluster ? buildRabbitPanels(selectedRabbitCluster.rabbitmq_cluster, queueFilter) : []), - ...(selectedValkeyCluster ? buildValkeyPanels(selectedValkeyCluster.valkey_cluster) : []), - ]; - setDashboardPanels(FeatureID, [], panels); - goForward?.(); - }; - - return ( - <> -
-

Edge Configuration

-

- Edge is our stateful services platform. This feature adds pre-built - panels scoped to edge service instances. -

-
- - - field="rabbitmq_cluster" - value={rabbitValue} - suggestions={filteredRabbitClusters} - completeMethod={searchRabbitClusters} - onChange={(e) => setRabbitValue(e.value)} - itemTemplate={rabbitTemplate} - selectedItemTemplate={(rabbit: Rabbit) => rabbit.rabbitmq_cluster} - dropdown - /> -
- {selectedRabbitCluster && ( -
-
- setQueueFilter(e.target.value)} - /> -
queues: {filteredQueues.length}
-
-
-
- {filteredQueues.map((q) => ( -
{q.queue}
- ))} -
-
-
- )} -
- - - field="valkey_cluster" - value={valkeyValue} - suggestions={filteredValkeyClusters} - completeMethod={searchValkeyClusters} - onChange={(e) => setValkeyValue(e.value)} - itemTemplate={valkeyTemplate} - selectedItemTemplate={(valkey: Valkey) => valkey.valkey_cluster} - dropdown - /> -
-
- -
- {goBack && ( - - )} - {goForward && ( - - )} -
- - ); -} diff --git a/src/features/rabbitmq.tsx b/src/features/rabbitmq.tsx new file mode 100644 index 0000000..a44d654 --- /dev/null +++ b/src/features/rabbitmq.tsx @@ -0,0 +1,202 @@ +import { useCallback, useEffect, useState } from "react"; +import { queryPrometheus } from "../lib/prometheusQuerier.ts"; +import { useAuth } from "react-oidc-context"; +import { AutoComplete } from "primereact/autocomplete"; +import { PanelBuilder as TimeSeriesPanelBuilder } from "@grafana/grafana-foundation-sdk/timeseries"; +import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus"; +import { useEnv } from "../components/env.tsx"; + +export const FeatureID = "rabbitmq"; +export const FeatureName = "RabbitMQ (Edge)"; +export const FeatureIcon = "https://www.rabbitmq.com/assets/files/rabbitmq-logo-e91cacd38fcef5219149bc5cfa10b384.svg"; + +// ---- Types ---- + +interface Rabbit { + rabbitmq_cluster: string; + novus_region: string; +} + +interface Queue { + queue: string; +} + +interface Props { + goBack?: () => void; + goForward?: () => void; + setDashboardPanels: (id: string, overview: any[], panels: any[]) => void; +} + +// ---- Data hook ---- + +function useRabbitData( + host: string | undefined, + token: string | undefined, + isAuthenticated: boolean, + selectedCluster: Rabbit | undefined, +) { + const [clusters, setClusters] = useState([]); + const [queues, setQueues] = useState([]); + + useEffect(() => { + if (!isAuthenticated) return; + queryPrometheus( + host, + "group(rabbitmq_identity_info{}) by (rabbitmq_cluster, novus_region)", + token, + ).then((result) => + setClusters( + result.data.result.map((item: any) => ({ + rabbitmq_cluster: item.metric.rabbitmq_cluster, + novus_region: item.metric.novus_region, + })), + ), + ); + }, [isAuthenticated]); + + useEffect(() => { + if (!isAuthenticated || !selectedCluster) { + setQueues([]); + return; + } + queryPrometheus( + host, + `group(rabbitmq_detailed_queue_messages{service="${selectedCluster.rabbitmq_cluster}"}) by (queue)`, + token, + ).then((result) => + setQueues( + result.data.result.map((item: any) => ({ queue: item.metric.queue })), + ), + ); + }, [isAuthenticated, selectedCluster]); + + return { clusters, queues }; +} + +// ---- Panel builders (pure, no React) ---- + +function buildPanels(cluster: string, queueFilter: string) { + return [ + new TimeSeriesPanelBuilder() + .title("Queues") + .height(8) + .span(8) + .withTarget( + new PrometheusDataqueryBuilder() + .datasource({ uid: "$prometheus" }) + .expr( + `sum(rabbitmq_detailed_queue_messages{service="${cluster}", queue=~"${queueFilter}"}) by (queue)`, + ) + .legendFormat(`{{ queue }}`), + ), + ]; +} + +// ---- Component ---- + +export function Component({ goBack, goForward, setDashboardPanels }: Props) { + const auth = useAuth(); + const env = useEnv(); + const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; + const token = auth?.user?.id_token; + + const [clusterValue, setClusterValue] = useState(); + const [filteredClusters, setFilteredClusters] = useState([]); + const [queueFilter, setQueueFilter] = useState(".*"); + + const selectedCluster = typeof clusterValue === "object" ? clusterValue : undefined; + + const { clusters, queues } = useRabbitData(host, token, !!auth?.isAuthenticated, selectedCluster); + + const filteredQueues = queues.filter((q) => q.queue.match(queueFilter)); + + const searchClusters = useCallback( + (event: { query: string }) => { + if (!event.query.trim()) { + setFilteredClusters(clusters); + return; + } + const terms = event.query.trim().split(/[-_\s]/); + setFilteredClusters( + clusters.filter((r) => + terms.every( + (t) => + r.rabbitmq_cluster.toLowerCase().includes(t) || + r.novus_region.toLowerCase().includes(t), + ), + ), + ); + }, + [clusters], + ); + + const clusterTemplate = (rabbit: Rabbit) => ( +
+ [{rabbit.novus_region.split("-")[0]?.toUpperCase()}] {rabbit.rabbitmq_cluster} +
+ ); + + const onSubmit = () => { + if (!selectedCluster) return; + setDashboardPanels(FeatureID, [], buildPanels(selectedCluster.rabbitmq_cluster, queueFilter)); + goForward?.(); + }; + + return ( + <> +
+

RabbitMQ Configuration

+

+ RabbitMQ running on Edge, our stateful services platform. This feature + adds pre-built panels scoped to a RabbitMQ cluster instance. +

+
+ + + field="rabbitmq_cluster" + value={clusterValue} + suggestions={filteredClusters} + completeMethod={searchClusters} + onChange={(e) => setClusterValue(e.value)} + itemTemplate={clusterTemplate} + selectedItemTemplate={(rabbit: Rabbit) => rabbit.rabbitmq_cluster} + dropdown + /> +
+ {selectedCluster && ( +
+
+ setQueueFilter(e.target.value)} + /> +
queues: {filteredQueues.length}
+
+
+
+ {filteredQueues.map((q) => ( +
{q.queue}
+ ))} +
+
+
+ )} +
+ +
+ {goBack && ( + + )} + {goForward && ( + + )} +
+ + ); +} diff --git a/src/features/valkey.tsx b/src/features/valkey.tsx new file mode 100644 index 0000000..7267e0d --- /dev/null +++ b/src/features/valkey.tsx @@ -0,0 +1,169 @@ +import { useCallback, useEffect, useState } from "react"; +import { queryPrometheus } from "../lib/prometheusQuerier.ts"; +import { useAuth } from "react-oidc-context"; +import { AutoComplete } from "primereact/autocomplete"; +import { PanelBuilder as TimeSeriesPanelBuilder } from "@grafana/grafana-foundation-sdk/timeseries"; +import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana-foundation-sdk/prometheus"; +import { useEnv } from "../components/env.tsx"; + +export const FeatureID = "valkey"; +export const FeatureName = "Valkey (Edge)"; +export const FeatureIcon = "https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/valkey.svg" + +// ---- Types ---- + +interface Valkey { + valkey_cluster: string; + novus_region: string; +} + +interface Props { + goBack?: () => void; + goForward?: () => void; + setDashboardPanels: (id: string, overview: any[], panels: any[]) => void; +} + +// ---- Data hook ---- + +const valkeyRegex = /^(.*?)(?:-(?:valkey-)?metrics)$/; + +function useValkeyData( + host: string | undefined, + token: string | undefined, + isAuthenticated: boolean, +) { + const [clusters, setClusters] = useState([]); + + useEffect(() => { + if (!isAuthenticated) return; + queryPrometheus( + host, + "group(redis_up{}) by (service, novus_region)", + token, + ).then((result) => + setClusters( + result.data.result.map((item: any) => ({ + valkey_cluster: + item.metric.service.match(valkeyRegex)?.[1] ?? + item.metric.service, + novus_region: item.metric.novus_region, + })), + ), + ); + }, [isAuthenticated]); + + return { clusters }; +} + +// ---- Panel builders (pure, no React) ---- + +function buildPanels(cluster: string) { + return [ + new TimeSeriesPanelBuilder() + .title(`${cluster}: Commands`) + .height(8) + .span(12) + .withTarget( + new PrometheusDataqueryBuilder() + .datasource({ uid: "$prometheus" }) + .expr(`sum(rate(redis_commands_total{service=~"${cluster}(-valkey)?-metrics"}[$__rate_interval])) by (cmd)`) + .legendFormat(`{{ cmd }}`), + ), + new TimeSeriesPanelBuilder() + .title(`${cluster}: Memory`) + .height(8) + .span(12) + .withTarget( + new PrometheusDataqueryBuilder() + .datasource({ uid: "$prometheus" }) + .expr(`redis_memory_used_bytes{service=~"${cluster}(-valkey)?-metrics"}`) + .legendFormat(`{{ instance }}`), + ), + ]; +} + +// ---- Component ---- + +export function Component({ goBack, goForward, setDashboardPanels }: Props) { + const auth = useAuth(); + const env = useEnv(); + const host = env?.["BUN_PUBLIC_PROMETHEUS_ENDPOINT"]; + const token = auth?.user?.id_token; + + const [clusterValue, setClusterValue] = useState(); + const [filteredClusters, setFilteredClusters] = useState([]); + + const selectedCluster = typeof clusterValue === "object" ? clusterValue : undefined; + + const { clusters } = useValkeyData(host, token, !!auth?.isAuthenticated); + + const searchClusters = useCallback( + (event: { query: string }) => { + if (!event.query.trim()) { + setFilteredClusters(clusters); + return; + } + const terms = event.query.trim().split(/[-_\s]/); + setFilteredClusters( + clusters.filter((v) => + terms.every( + (t) => + v.valkey_cluster.toLowerCase().includes(t) || + v.novus_region.toLowerCase().includes(t), + ), + ), + ); + }, + [clusters], + ); + + const clusterTemplate = (valkey: Valkey) => ( +
+ [{valkey.novus_region.split("-")[0]?.toUpperCase()}] {valkey.valkey_cluster} +
+ ); + + const onSubmit = () => { + if (!selectedCluster) return; + setDashboardPanels(FeatureID, [], buildPanels(selectedCluster.valkey_cluster)); + goForward?.(); + }; + + return ( + <> +
+

Valkey Configuration

+

+ Valkey running on Edge, our stateful services platform. This feature + adds pre-built panels scoped to a Valkey cluster instance. +

+
+ + + field="valkey_cluster" + value={clusterValue} + suggestions={filteredClusters} + completeMethod={searchClusters} + onChange={(e) => setClusterValue(e.value)} + itemTemplate={clusterTemplate} + selectedItemTemplate={(valkey: Valkey) => valkey.valkey_cluster} + dropdown + /> +
+
+ +
+ {goBack && ( + + )} + {goForward && ( + + )} +
+ + ); +} From d7dd323843202e7e67a7f40f62f0126196be3605 Mon Sep 17 00:00:00 2001 From: Paul Farver Date: Mon, 20 Apr 2026 08:14:26 +0200 Subject: [PATCH 5/5] chore: Update edge feature names --- src/features/rabbitmq.tsx | 2 +- src/features/valkey.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/rabbitmq.tsx b/src/features/rabbitmq.tsx index a44d654..fb662b4 100644 --- a/src/features/rabbitmq.tsx +++ b/src/features/rabbitmq.tsx @@ -7,7 +7,7 @@ import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana import { useEnv } from "../components/env.tsx"; export const FeatureID = "rabbitmq"; -export const FeatureName = "RabbitMQ (Edge)"; +export const FeatureName = "Edge RabbitMQ"; export const FeatureIcon = "https://www.rabbitmq.com/assets/files/rabbitmq-logo-e91cacd38fcef5219149bc5cfa10b384.svg"; // ---- Types ---- diff --git a/src/features/valkey.tsx b/src/features/valkey.tsx index 7267e0d..fb37ab9 100644 --- a/src/features/valkey.tsx +++ b/src/features/valkey.tsx @@ -7,7 +7,7 @@ import { DataqueryBuilder as PrometheusDataqueryBuilder } from "@grafana/grafana import { useEnv } from "../components/env.tsx"; export const FeatureID = "valkey"; -export const FeatureName = "Valkey (Edge)"; +export const FeatureName = "Edge Valkey"; export const FeatureIcon = "https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/valkey.svg" // ---- Types ----