Merge branch 'main' into release/v1.5

This commit is contained in:
2026-06-04 17:56:41 +02:00
5 changed files with 54 additions and 19 deletions

View File

@@ -238,6 +238,9 @@ func buildFilterPresets(query url.Values) map[string]any {
if query.Get("cluster") != "" { if query.Get("cluster") != "" {
filterPresets["cluster"] = query.Get("cluster") filterPresets["cluster"] = query.Get("cluster")
} }
if query.Get("subCluster") != "" {
filterPresets["subCluster"] = query.Get("subCluster")
}
if query.Get("partition") != "" { if query.Get("partition") != "" {
filterPresets["partition"] = query.Get("partition") filterPresets["partition"] = query.Get("partition")
} }

View File

@@ -78,7 +78,7 @@ type APIQueryResponse struct {
// - Type + TypeIds: First level of hierarchy (e.g., "cpu" + ["0", "1", "2"]) // - Type + TypeIds: First level of hierarchy (e.g., "cpu" + ["0", "1", "2"])
// - SubType + SubTypeIds: Second level of hierarchy (e.g., "core" + ["0", "1"]) // - 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. // to the metric's aggregation strategy. Otherwise, separate results are returned for each combination.
type APIQuery struct { type APIQuery struct {
Type *string `json:"type,omitempty"` 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: // This is the primary API for retrieving metric data from the memory store. It supports:
// - Individual queries via req.Queries // - Individual queries via req.Queries
// - Batch queries for all nodes via req.ForAllNodes // - 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 statistics computation (avg, min, max)
// - Optional data scaling // - Optional data scaling
// - Optional data padding with NaN values // - Optional data padding with NaN values
// //
// The function constructs selectors based on the query parameters and calls MemoryStore.Read() // 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. // separate results are returned for each combination.
// //
// Parameters: // Parameters:

View File

@@ -6,11 +6,11 @@
// This file implements ingestion of InfluxDB line-protocol metric data received // This file implements ingestion of InfluxDB line-protocol metric data received
// over NATS. Each line encodes one metric sample with the following structure: // over NATS. Each line encodes one metric sample with the following structure:
// //
// <measurement>[,cluster=<c>][,hostname=<h>][,type=<t>][,type-id=<id>][,subtype=<s>][,stype-id=<id>] value=<v> [<timestamp>] // <measurement>[,cluster=<c>][,hostname=<h>][,type=<t>][,type-id=<id>][,stype=<s>][,stype-id=<id>] value=<v> [<timestamp>]
// //
// The measurement name identifies the metric (e.g. "cpu_load"). Tags provide // The measurement name identifies the metric (e.g. "cpu_load"). Tags provide
// routing information (cluster, host) and optional sub-device selectors (type, // 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: // After decoding, each sample is:
// 1. Written to the in-memory store via ms.WriteToLevel. // 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, // reorder prepends prefix to buf in-place when buf has enough spare capacity,
// avoiding an allocation. Falls back to a regular append otherwise. // avoiding an allocation. Falls back to a regular append otherwise.
// //
// It is used to assemble the "type<type-id>" and "subtype<stype-id>" selector // It is used to assemble the "type<type-id>" and "stype<stype-id>" selector
// strings when the type tag arrives before the type-id tag in the line, so the // 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 // two byte slices need to be concatenated in tag-declaration order regardless
// of wire order. // of wire order.
@@ -145,7 +145,7 @@ type decodeState struct {
// current line. Reset at the start of each line's tag-decode loop. // current line. Reset at the start of each line's tag-decode loop.
typeBuf []byte 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. // Reset at the start of each line's tag-decode loop.
subTypeBuf []byte subTypeBuf []byte
@@ -186,7 +186,7 @@ var decodeStatePool = sync.Pool{
// - The Level pointer (host-level node in the metric tree) is cached across // - The Level pointer (host-level node in the metric tree) is cached across
// consecutive lines that share the same cluster+host pair to avoid // consecutive lines that share the same cluster+host pair to avoid
// repeated lock acquisitions on the root and cluster levels. // 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 // prevType*/prevSubType* fields because batches typically repeat the same
// sub-device identifiers. // sub-device identifiers.
// - Timestamp parsing tries Second precision first; if that fails it retries // - Timestamp parsing tries Second precision first; if that fails it retries
@@ -269,8 +269,8 @@ func DecodeLine(dec *lineprotocol.Decoder,
} }
case "type-id": case "type-id":
st.typeBuf = append(st.typeBuf, val...) st.typeBuf = append(st.typeBuf, val...)
case "subtype": case "stype":
// We cannot be sure that the "subtype" tag comes before the "stype-id" tag: // We cannot be sure that the "stype" tag comes before the "stype-id" tag:
if len(st.subTypeBuf) == 0 { if len(st.subTypeBuf) == 0 {
st.subTypeBuf = append(st.subTypeBuf, val...) st.subTypeBuf = append(st.subTypeBuf, val...)
} else { } else {
@@ -291,7 +291,7 @@ func DecodeLine(dec *lineprotocol.Decoder,
} }
// subtypes: cache []byte→string conversions; messages in a batch typically // 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] st.selector = st.selector[:0]
if len(st.typeBuf) > 0 { if len(st.typeBuf) > 0 {
if !bytes.Equal(st.typeBuf, st.prevTypeBytes) { if !bytes.Equal(st.typeBuf, st.prevTypeBytes) {

View File

@@ -73,6 +73,7 @@
userMatch: "contains", userMatch: "contains",
// Filter Modals // Filter Modals
cluster: null, cluster: null,
subCluster: null,
partition: null, partition: null,
states: allJobStates, states: allJobStates,
shared: "", shared: "",
@@ -107,6 +108,7 @@
user: filterPresets?.user || "", user: filterPresets?.user || "",
userMatch: filterPresets?.userMatch || "contains", userMatch: filterPresets?.userMatch || "contains",
cluster: filterPresets?.cluster || null, cluster: filterPresets?.cluster || null,
subCluster: filterPresets?.subCluster || null,
partition: filterPresets?.partition || null, partition: filterPresets?.partition || null,
states: states:
filterPresets?.states || filterPresets?.state filterPresets?.states || filterPresets?.state
@@ -158,6 +160,7 @@
if (filters.dbId.length != 0) if (filters.dbId.length != 0)
items.push({ dbId: filters.dbId }); items.push({ dbId: filters.dbId });
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } }); 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.partition) items.push({ partition: { eq: filters.partition } });
if (filters.states.length != allJobStates?.length) if (filters.states.length != allJobStates?.length)
items.push({ state: filters.states }); items.push({ state: filters.states });
@@ -267,6 +270,7 @@
opts.push(`userMatch=${filters.userMatch}`); opts.push(`userMatch=${filters.userMatch}`);
// Filter Modals // Filter Modals
if (filters.cluster) opts.push(`cluster=${filters.cluster}`); 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.partition) opts.push(`partition=${filters.partition}`);
if (filters.states.length != allJobStates?.length) if (filters.states.length != allJobStates?.length)
for (let state of filters.states) opts.push(`state=${state}`); for (let state of filters.states) opts.push(`state=${state}`);
@@ -346,7 +350,7 @@
{/if} {/if}
<DropdownItem header>Manage Filters</DropdownItem> <DropdownItem header>Manage Filters</DropdownItem>
<DropdownItem onclick={() => (isClusterOpen = true)}> <DropdownItem onclick={() => (isClusterOpen = true)}>
<Icon name="cpu" /> Cluster/Partition <Icon name="cpu" /> Cluster/SubCluster/Partition
</DropdownItem> </DropdownItem>
<DropdownItem onclick={() => (isJobStatesOpen = true)}> <DropdownItem onclick={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill" /> Job States <Icon name="gear-fill" /> Job States
@@ -440,6 +444,9 @@
{#if filters.cluster} {#if filters.cluster}
<Info icon="cpu" onclick={() => (isClusterOpen = true)}> <Info icon="cpu" onclick={() => (isClusterOpen = true)}>
{filters.cluster} {filters.cluster}
{#if filters.subCluster}
[{filters.subCluster}]
{/if}
{#if filters.partition} {#if filters.partition}
({filters.partition}) ({filters.partition})
{/if} {/if}
@@ -603,6 +610,7 @@
bind:isOpen={isClusterOpen} bind:isOpen={isClusterOpen}
presetCluster={filters.cluster} presetCluster={filters.cluster}
presetPartition={filters.partition} presetPartition={filters.partition}
presetSubCluster={filters.subCluster}
{disableClusterSelection} {disableClusterSelection}
setFilter={(filter) => updateFilters(filter)} setFilter={(filter) => updateFilters(filter)}
/> />

View File

@@ -1,10 +1,11 @@
<!-- <!--
@component Filter sub-component for selecting cluster and subCluster @component Filter sub-component for selecting cluster, partition and subCluster
Properties: Properties:
- `isOpen Bool?`: Is this filter component opened [Bindable, Default: false] - `isOpen Bool?`: Is this filter component opened [Bindable, Default: false]
- `presetCluster String?`: The latest selected cluster [Default: ""] - `presetCluster String?`: The latest selected cluster [Default: ""]
- `presetPartition String?`: The latest selected partition [Default: ""] - `presetPartition String?`: The latest selected partition [Default: ""]
- `presetSubCluster String?`: The latest selected subCluster [Default: ""]
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false] - `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
- `setFilter Func`: The callback function to apply current filter selection - `setFilter Func`: The callback function to apply current filter selection
--> -->
@@ -26,6 +27,7 @@
isOpen = $bindable(false), isOpen = $bindable(false),
presetCluster = "", presetCluster = "",
presetPartition = "", presetPartition = "",
presetSubCluster = "",
disableClusterSelection = false, disableClusterSelection = false,
setFilter setFilter
} = $props(); } = $props();
@@ -36,10 +38,11 @@
const clusterInfos = $derived($initialized ? getContext("clusters") : null); const clusterInfos = $derived($initialized ? getContext("clusters") : null);
let pendingCluster = $derived(presetCluster); let pendingCluster = $derived(presetCluster);
let pendingPartition = $derived(presetPartition); let pendingPartition = $derived(presetPartition);
let pendingSubCluster = $derived(presetSubCluster);
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Cluster & Slurm Partition</ModalHeader> <ModalHeader>Select Cluster, SubCluster & Partition</ModalHeader>
<ModalBody> <ModalBody>
{#if $initialized} {#if $initialized}
<h4>Cluster</h4> <h4>Cluster</h4>
@@ -51,7 +54,7 @@
<ListGroupItem <ListGroupItem
disabled={disableClusterSelection} disabled={disableClusterSelection}
active={pendingCluster == null} active={pendingCluster == null}
onclick={() => ((pendingCluster = null), (pendingPartition = null))} onclick={() => ((pendingCluster = null), (pendingPartition = null), (pendingSubCluster = null))}
> >
Any Cluster Any Cluster
</ListGroupItem> </ListGroupItem>
@@ -60,7 +63,7 @@
disabled={disableClusterSelection} disabled={disableClusterSelection}
active={pendingCluster == cluster.name} active={pendingCluster == cluster.name}
onclick={() => ( onclick={() => (
(pendingCluster = cluster.name), (pendingPartition = null) (pendingCluster = cluster.name), (pendingPartition = null), (pendingSubCluster = null)
)} )}
> >
{cluster.name} {cluster.name}
@@ -71,7 +74,27 @@
{/if} {/if}
{#if $initialized && pendingCluster != null} {#if $initialized && pendingCluster != null}
<br /> <br />
<h4>Partiton</h4> <h4>SubCluster</h4>
<ListGroup>
<ListGroupItem
active={pendingSubCluster == null}
onclick={() => (pendingSubCluster = null)}
>
Any SubCluster
</ListGroupItem>
{#each clusterInfos?.find((c) => c.name == pendingCluster)?.subClusters as subCluster}
<ListGroupItem
active={pendingSubCluster == subCluster.name}
onclick={() => (pendingSubCluster = subCluster.name)}
>
{subCluster.name}
</ListGroupItem>
{/each}
</ListGroup>
{/if}
{#if $initialized && pendingCluster != null}
<br />
<h4>Partition</h4>
<ListGroup> <ListGroup>
<ListGroupItem <ListGroupItem
active={pendingPartition == null} active={pendingPartition == null}
@@ -95,7 +118,7 @@
color="primary" color="primary"
onclick={() => { onclick={() => {
isOpen = false; isOpen = false;
setFilter({ cluster: pendingCluster, partition: pendingPartition }); setFilter({ cluster: pendingCluster, subCluster: pendingSubCluster, partition: pendingPartition });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
{#if !disableClusterSelection} {#if !disableClusterSelection}
@@ -105,7 +128,8 @@
isOpen = false; isOpen = false;
pendingCluster = null; pendingCluster = null;
pendingPartition = null; pendingPartition = null;
setFilter({ cluster: pendingCluster, partition: pendingPartition}) pendingSubCluster = null;
setFilter({ cluster: pendingCluster, subCluster: pendingSubCluster, partition: pendingPartition })
}}>Reset</Button }}>Reset</Button
> >
{/if} {/if}