From 4c59aee304e75a1e9276681df48c6bb2928834ce Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 24 Apr 2026 11:43:42 +0200 Subject: [PATCH 1/2] feat: add subCluster filter to filter component --- internal/routerConfig/routes.go | 3 ++ web/frontend/src/generic/Filters.svelte | 10 ++++- .../src/generic/filters/Cluster.svelte | 38 +++++++++++++++---- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go index 78bab931..db499397 100644 --- a/internal/routerConfig/routes.go +++ b/internal/routerConfig/routes.go @@ -238,6 +238,9 @@ func buildFilterPresets(query url.Values) map[string]any { if query.Get("cluster") != "" { filterPresets["cluster"] = query.Get("cluster") } + if query.Get("subCluster") != "" { + filterPresets["subCluster"] = query.Get("subCluster") + } if query.Get("partition") != "" { filterPresets["partition"] = query.Get("partition") } diff --git a/web/frontend/src/generic/Filters.svelte b/web/frontend/src/generic/Filters.svelte index 3c0091ad..427eabdc 100644 --- a/web/frontend/src/generic/Filters.svelte +++ b/web/frontend/src/generic/Filters.svelte @@ -73,6 +73,7 @@ userMatch: "contains", // Filter Modals cluster: null, + subCluster: null, partition: null, states: allJobStates, shared: "", @@ -107,6 +108,7 @@ user: filterPresets?.user || "", userMatch: filterPresets?.userMatch || "contains", cluster: filterPresets?.cluster || null, + subCluster: filterPresets?.subCluster || null, partition: filterPresets?.partition || null, states: filterPresets?.states || filterPresets?.state @@ -158,6 +160,7 @@ if (filters.dbId.length != 0) items.push({ dbId: filters.dbId }); if (filters.cluster) items.push({ cluster: { eq: filters.cluster } }); + if (filters.subCluster) items.push({ subCluster: { eq: filters.subCluster } }); if (filters.partition) items.push({ partition: { eq: filters.partition } }); if (filters.states.length != allJobStates?.length) items.push({ state: filters.states }); @@ -267,6 +270,7 @@ opts.push(`userMatch=${filters.userMatch}`); // Filter Modals if (filters.cluster) opts.push(`cluster=${filters.cluster}`); + if (filters.subCluster) opts.push(`subCluster=${filters.subCluster}`); if (filters.partition) opts.push(`partition=${filters.partition}`); if (filters.states.length != allJobStates?.length) for (let state of filters.states) opts.push(`state=${state}`); @@ -346,7 +350,7 @@ {/if} Manage Filters (isClusterOpen = true)}> - Cluster/Partition + Cluster/SubCluster/Partition (isJobStatesOpen = true)}> Job States @@ -440,6 +444,9 @@ {#if filters.cluster} (isClusterOpen = true)}> {filters.cluster} + {#if filters.subCluster} + [{filters.subCluster}] + {/if} {#if filters.partition} ({filters.partition}) {/if} @@ -603,6 +610,7 @@ bind:isOpen={isClusterOpen} presetCluster={filters.cluster} presetPartition={filters.partition} + presetSubCluster={filters.subCluster} {disableClusterSelection} setFilter={(filter) => updateFilters(filter)} /> diff --git a/web/frontend/src/generic/filters/Cluster.svelte b/web/frontend/src/generic/filters/Cluster.svelte index bb02335e..4736ead1 100644 --- a/web/frontend/src/generic/filters/Cluster.svelte +++ b/web/frontend/src/generic/filters/Cluster.svelte @@ -1,10 +1,11 @@ @@ -26,6 +27,7 @@ isOpen = $bindable(false), presetCluster = "", presetPartition = "", + presetSubCluster = "", disableClusterSelection = false, setFilter } = $props(); @@ -36,10 +38,11 @@ const clusterInfos = $derived($initialized ? getContext("clusters") : null); let pendingCluster = $derived(presetCluster); let pendingPartition = $derived(presetPartition); + let pendingSubCluster = $derived(presetSubCluster); (isOpen = !isOpen)}> - Select Cluster & Slurm Partition + Select Cluster, SubCluster & Partition {#if $initialized}

Cluster

@@ -51,7 +54,7 @@ ((pendingCluster = null), (pendingPartition = null))} + onclick={() => ((pendingCluster = null), (pendingPartition = null), (pendingSubCluster = null))} > Any Cluster @@ -60,7 +63,7 @@ disabled={disableClusterSelection} active={pendingCluster == cluster.name} onclick={() => ( - (pendingCluster = cluster.name), (pendingPartition = null) + (pendingCluster = cluster.name), (pendingPartition = null), (pendingSubCluster = null) )} > {cluster.name} @@ -71,7 +74,27 @@ {/if} {#if $initialized && pendingCluster != null}
-

Partiton

+

SubCluster

+ + (pendingSubCluster = null)} + > + Any SubCluster + + {#each clusterInfos?.find((c) => c.name == pendingCluster)?.subClusters as subCluster} + (pendingSubCluster = subCluster.name)} + > + {subCluster.name} + + {/each} + + {/if} + {#if $initialized && pendingCluster != null} +
+

Partition

{ isOpen = false; - setFilter({ cluster: pendingCluster, partition: pendingPartition }); + setFilter({ cluster: pendingCluster, subCluster: pendingSubCluster, partition: pendingPartition }); }}>Close & Apply {#if !disableClusterSelection} @@ -105,7 +128,8 @@ isOpen = false; pendingCluster = null; pendingPartition = null; - setFilter({ cluster: pendingCluster, partition: pendingPartition}) + pendingSubCluster = null; + setFilter({ cluster: pendingCluster, subCluster: pendingSubCluster, partition: pendingPartition }) }}>Reset {/if} From c76219651e7783884eb5bd1b5702490b59c25b8d Mon Sep 17 00:00:00 2001 From: Thomas Roehl Date: Mon, 4 May 2026 18:10:01 +0200 Subject: [PATCH 2/2] Fix parsing of metric subtypes (key stype) --- pkg/metricstore/api.go | 6 +++--- pkg/metricstore/lineprotocol.go | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/metricstore/api.go b/pkg/metricstore/api.go index 01dec633..8e02f44c 100644 --- a/pkg/metricstore/api.go +++ b/pkg/metricstore/api.go @@ -78,7 +78,7 @@ type APIQueryResponse struct { // - Type + TypeIds: First level of hierarchy (e.g., "cpu" + ["0", "1", "2"]) // - SubType + SubTypeIds: Second level of hierarchy (e.g., "core" + ["0", "1"]) // -// If Aggregate is true, data from multiple type/subtype IDs will be aggregated according +// If Aggregate is true, data from multiple type/stype IDs will be aggregated according // to the metric's aggregation strategy. Otherwise, separate results are returned for each combination. type APIQuery struct { Type *string `json:"type,omitempty"` @@ -174,13 +174,13 @@ func (data *APIMetricData) PadDataWithNull(ms *MemoryStore, from, to int64, metr // This is the primary API for retrieving metric data from the memory store. It supports: // - Individual queries via req.Queries // - Batch queries for all nodes via req.ForAllNodes -// - Hierarchical selector construction (cluster → host → type → subtype) +// - Hierarchical selector construction (cluster → host → type → stype) // - Optional statistics computation (avg, min, max) // - Optional data scaling // - Optional data padding with NaN values // // The function constructs selectors based on the query parameters and calls MemoryStore.Read() -// for each selector. If a query specifies Aggregate=false with multiple type/subtype IDs, +// for each selector. If a query specifies Aggregate=false with multiple type/stype IDs, // separate results are returned for each combination. // // Parameters: diff --git a/pkg/metricstore/lineprotocol.go b/pkg/metricstore/lineprotocol.go index caec82e9..ed42eead 100644 --- a/pkg/metricstore/lineprotocol.go +++ b/pkg/metricstore/lineprotocol.go @@ -6,11 +6,11 @@ // This file implements ingestion of InfluxDB line-protocol metric data received // over NATS. Each line encodes one metric sample with the following structure: // -// [,cluster=][,hostname=][,type=][,type-id=][,subtype=][,stype-id=] value= [] +// [,cluster=][,hostname=][,type=][,type-id=][,stype=][,stype-id=] value= [] // // The measurement name identifies the metric (e.g. "cpu_load"). Tags provide // routing information (cluster, host) and optional sub-device selectors (type, -// subtype). Only one field is expected per line: "value". +// stype). Only one field is expected per line: "value". // // After decoding, each sample is: // 1. Written to the in-memory store via ms.WriteToLevel. @@ -103,7 +103,7 @@ func ReceiveNats(ms *MemoryStore, // reorder prepends prefix to buf in-place when buf has enough spare capacity, // avoiding an allocation. Falls back to a regular append otherwise. // -// It is used to assemble the "type" and "subtype" selector +// It is used to assemble the "type" and "stype" selector // strings when the type tag arrives before the type-id tag in the line, so the // two byte slices need to be concatenated in tag-declaration order regardless // of wire order. @@ -145,7 +145,7 @@ type decodeState struct { // current line. Reset at the start of each line's tag-decode loop. typeBuf []byte - // subTypeBuf accumulates the concatenated "subtype"+"stype-id" tag value. + // subTypeBuf accumulates the concatenated "stype"+"stype-id" tag value. // Reset at the start of each line's tag-decode loop. subTypeBuf []byte @@ -186,7 +186,7 @@ var decodeStatePool = sync.Pool{ // - The Level pointer (host-level node in the metric tree) is cached across // consecutive lines that share the same cluster+host pair to avoid // repeated lock acquisitions on the root and cluster levels. -// - []byte→string conversions for type/subtype selectors are cached via +// - []byte→string conversions for type/stype selectors are cached via // prevType*/prevSubType* fields because batches typically repeat the same // sub-device identifiers. // - Timestamp parsing tries Second precision first; if that fails it retries @@ -269,8 +269,8 @@ func DecodeLine(dec *lineprotocol.Decoder, } case "type-id": st.typeBuf = append(st.typeBuf, val...) - case "subtype": - // We cannot be sure that the "subtype" tag comes before the "stype-id" tag: + case "stype": + // We cannot be sure that the "stype" tag comes before the "stype-id" tag: if len(st.subTypeBuf) == 0 { st.subTypeBuf = append(st.subTypeBuf, val...) } else { @@ -291,7 +291,7 @@ func DecodeLine(dec *lineprotocol.Decoder, } // subtypes: cache []byte→string conversions; messages in a batch typically - // share the same type/subtype so the hit rate is very high. + // share the same type/stype so the hit rate is very high. st.selector = st.selector[:0] if len(st.typeBuf) > 0 { if !bytes.Equal(st.typeBuf, st.prevTypeBytes) {