change: remove heuristic metricHealth, replace with DB metricHealth

- add metricHealth to single Node view
This commit is contained in:
Christoph Kluge
2026-03-19 15:55:58 +01:00
parent 886791cf8a
commit 10b4fa5a06
12 changed files with 171 additions and 104 deletions

View File

@@ -130,7 +130,7 @@
name
count
}
# Get Current States fir Pie Charts
# Get Current States for Pie Charts
nodeStates(filter: $nodeFilter) {
state
count

View File

@@ -57,7 +57,8 @@
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
host
state
nodeState
metricHealth
subCluster
metrics {
name
@@ -92,7 +93,7 @@
}
}
`;
// Node State Colors
// Node/Metric State Colors
const stateColors = {
allocated: 'success',
reserved: 'info',
@@ -100,7 +101,10 @@
mixed: 'warning',
down: 'danger',
unknown: 'dark',
notindb: 'secondary'
notindb: 'secondary',
full: 'success',
partial: 'warning',
failed: 'danger'
}
/* State Init */
@@ -153,31 +157,46 @@
})
);
const thisNodeState = $derived($nodeMetricsData?.data?.nodeMetrics[0]?.state ? $nodeMetricsData.data.nodeMetrics[0].state : 'notindb');
const thisNodeState = $derived($nodeMetricsData?.data?.nodeMetrics[0]?.nodeState || 'notindb');
const thisMetricHealth = $derived($nodeMetricsData?.data?.nodeMetrics[0]?.metricHealth || 'unknown');
</script>
<Row cols={{ xs: 2, lg: 5 }}>
<Row cols={{ xs: 2, lg: 3}}>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.fetching}
<Spinner />
{:else}
<!-- Node Col -->
<Col>
<Col class="mb-2">
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Selected Node</InputGroupText>
<Input style="background-color: white;" type="text" value="{hostname} [{cluster} {$nodeMetricsData?.data?.nodeMetrics[0] ? `(${$nodeMetricsData.data.nodeMetrics[0].subCluster})` : ''}]" disabled/>
</InputGroup>
</Col>
<!-- State Col -->
<Col>
<!-- Node State Col -->
<Col class="mb-2">
<InputGroup>
<InputGroupText><Icon name="clipboard2-pulse" /></InputGroupText>
<InputGroupText>Node State</InputGroupText>
<Button class="flex-grow-1 text-center" color={stateColors[thisNodeState]} disabled>
{#if $nodeMetricsData?.data}
{thisNodeState}
{thisNodeState.charAt(0).toUpperCase() + thisNodeState.slice(1)}
{:else}
<span><Spinner size="sm" secondary/></span>
{/if}
</Button>
</InputGroup>
</Col>
<!-- Metric Health Col -->
<Col class="mb-2">
<InputGroup>
<InputGroupText><Icon name="clipboard2-pulse" /></InputGroupText>
<InputGroupText>Metric Health</InputGroupText>
<Button class="flex-grow-1 text-center" color={stateColors[thisMetricHealth]} disabled>
{#if $nodeMetricsData?.data}
{thisMetricHealth.charAt(0).toUpperCase() + thisMetricHealth.slice(1)}
{:else}
<span><Spinner size="sm" secondary/></span>
{/if}
@@ -185,7 +204,7 @@
</InputGroup>
</Col>
<!-- Concurrent Col -->
<Col class="mt-2 mt-lg-0">
<Col>
{#if $nodeJobsData.fetching}
<Spinner />
{:else if $nodeJobsData.data}
@@ -217,7 +236,7 @@
/>
</Col>
<!-- Refresh Col-->
<Col class="mt-2 mt-lg-0">
<Col>
<Refresher
onRefresh={() => {
const diff = Date.now() - to;

View File

@@ -59,7 +59,7 @@
/* State Init */
let hostnameFilter = $state("");
let hoststateFilter = $state("all");
let nodeStateFilter = $state("all");
let pendingHostnameFilter = $state("");
let isMetricsSelectionOpen = $state(false);
@@ -210,7 +210,7 @@
<InputGroup>
<InputGroupText><Icon name="clipboard2-pulse" /></InputGroupText>
<InputGroupText>State</InputGroupText>
<Input type="select" bind:value={hoststateFilter}>
<Input type="select" bind:value={nodeStateFilter}>
{#each stateOptions as so}
<option value={so}>{so.charAt(0).toUpperCase() + so.slice(1)}</option>
{/each}
@@ -269,11 +269,11 @@
{:else}
{#if displayNodeOverview}
<!-- ROW2-1: Node Overview (Grid Included)-->
<NodeOverview {cluster} {ccconfig} {selectedMetric} {globalMetrics} {from} {to} {hostnameFilter} {hoststateFilter}/>
<NodeOverview {cluster} {ccconfig} {selectedMetric} {globalMetrics} {from} {to} {hostnameFilter} {nodeStateFilter}/>
{:else}
<!-- ROW2-2: Node List (Grid Included)-->
<NodeList pendingSelectedMetrics={selectedMetrics} {cluster} {subCluster}
{selectedResolution} {hostnameFilter} {hoststateFilter} {from} {to} {systemUnits}/>
{selectedResolution} {hostnameFilter} {nodeStateFilter} {from} {to} {systemUnits}/>
{/if}
{/if}

View File

@@ -6,8 +6,8 @@
- `subCluster String`: The nodes' subCluster [Default: ""]
- `pendingSelectedMetrics [String]`: The array of selected metrics [Default []]
- `selectedResolution Number?`: The selected data resolution [Default: 0]
- `hostnameFilter String?`: The active hostnamefilter [Default: ""]
- `hoststateFilter String?`: The active hoststatefilter [Default: ""]
- `hostnameFilter String?`: The active hostname filter [Default: ""]
- `nodeStateFilter String?`: The active nodeState filter [Default: ""]
- `systemUnits Object`: The object of metric units [Default: null]
- `from Date?`: The selected "from" date [Default: null]
- `to Date?`: The selected "to" date [Default: null]
@@ -28,7 +28,7 @@
pendingSelectedMetrics = [],
selectedResolution = 0,
hostnameFilter = "",
hoststateFilter = "",
nodeStateFilter = "",
systemUnits = null,
from = null,
to = null
@@ -54,7 +54,8 @@
) {
items {
host
state
nodeState
metricHealth
subCluster
metrics {
name
@@ -110,7 +111,7 @@
variables: {
cluster: cluster,
subCluster: subCluster,
stateFilter: hoststateFilter,
stateFilter: nodeStateFilter,
nodeFilter: hostnameFilter,
scopes: ["core", "socket", "accelerator"],
metrics: pendingSelectedMetrics,
@@ -164,7 +165,7 @@
$effect(() => {
// Update NodeListRows metrics only: Keep ordered nodes on page 1
hostnameFilter, hoststateFilter
hostnameFilter, nodeStateFilter
// Continous Scroll: Paging if parameters change: Existing entries will not match new selections
nodes = [];
if (!usePaging) {

View File

@@ -5,8 +5,8 @@
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
- `cluster String`: The cluster to show status information for
- `selectedMetric String?`: The selectedMetric input [Default: ""]
- `hostnameFilter String?`: The active hostnamefilter [Default: ""]
- `hostnameFilter String?`: The active hoststatefilter [Default: ""]
- `hostnameFilter String?`: The active hostname filter [Default: ""]
- `nodeStateFilter String?`: The active nodeState filter [Default: ""]
- `from Date?`: The selected "from" date [Default: null]
- `to Date?`: The selected "to" date [Default: null]
- `globalMetrics [Obj]`: Includes the backend supplied availabilities for cluster and subCluster
@@ -24,7 +24,7 @@
cluster = "",
selectedMetric = "",
hostnameFilter = "",
hoststateFilter = "",
nodeStateFilter = "",
from = null,
to = null,
globalMetrics
@@ -55,7 +55,7 @@
to: $to
) {
host
state
nodeState
subCluster
metrics {
name
@@ -91,11 +91,11 @@
const mappedData = $derived(handleQueryData($nodesQuery?.data));
const filteredData = $derived(mappedData.filter((h) => {
if (hostnameFilter) {
if (hoststateFilter == 'all') return h.host.includes(hostnameFilter)
else return (h.host.includes(hostnameFilter) && h.state == hoststateFilter)
if (nodeStateFilter == 'all') return h.host.includes(hostnameFilter)
else return (h.host.includes(hostnameFilter) && h.nodeState == nodeStateFilter)
} else {
if (hoststateFilter == 'all') return true
else return h.state == hoststateFilter
if (nodeStateFilter == 'all') return true
else return h.nodeState == nodeStateFilter
}
}));
@@ -116,7 +116,7 @@
if (rawData.length > 0) {
pendingMapped = rawData.map((h) => ({
host: h.host,
state: h?.state? h.state : 'notindb',
nodeState: h?.nodeState || 'notindb',
subCluster: h.subCluster,
data: h.metrics.filter(
(m) => m?.name == selectedMetric && m.scope == "node",
@@ -157,8 +157,8 @@
>
</h4>
<span style="margin-right: 0.5rem;">
<Badge color={stateColors[item?.state? item.state : 'notindb']}>
State: {item?.state? item.state.charAt(0).toUpperCase() + item.state.slice(1) : 'Not in DB'}
<Badge color={stateColors[item?.nodeState || 'notindb']}>
State: {item?.nodeState ? item.nodeState.charAt(0).toUpperCase() + item.nodeState.slice(1) : 'Not in DB'}
</Badge>
</span>
</div>
@@ -202,7 +202,7 @@
{/each}
{/key}
</Row>
{:else if hostnameFilter || hoststateFilter != 'all'}
{:else if hostnameFilter || nodeStateFilter != 'all'}
<Row class="mx-1">
<Card class="px-0">
<CardHeader>

View File

@@ -5,7 +5,8 @@
- `cluster String`: The nodes' cluster
- `subCluster String`: The nodes' subCluster
- `hostname String`: The nodes' hostname
- `dataHealth [Bool]`: Array of Booleans depicting state of returned data per metric
- `nodeState String`: The nodes current state as reported by the scheduler
- `metricHealth String`: The nodes current metric health as reported by the metricstore
- `nodeJobsData [Object]`: Data returned by GQL for jobs runninig on this node [Default: null]
-->
@@ -32,8 +33,8 @@
cluster,
subCluster,
hostname,
hoststate,
dataHealth,
nodeState,
metricHealth,
nodeJobsData = null,
} = $props();
@@ -50,12 +51,6 @@
}
/* Derived */
// Not at least one returned, selected metric: NodeHealth warning
const fetchInfo = $derived(dataHealth.includes('fetching'));
// Not at least one returned, selected metric: NodeHealth warning
const healthWarn = $derived(!dataHealth.includes(true));
// At least one non-returned selected metric: Metric config error?
const metricWarn = $derived(dataHealth.includes(false));
const userList = $derived(nodeJobsData
? Array.from(new Set(nodeJobsData.jobs.items.map((j) => scrambleNames ? scramble(j.user) : j.user))).sort((a, b) => a.localeCompare(b))
: []
@@ -86,14 +81,7 @@
<Row cols={{xs: 1, lg: 2}}>
<Col class="mb-2 mb-lg-0">
<InputGroup size="sm">
{#if fetchInfo}
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
<Icon name="arrow-clockwise" style="padding-right: 0.5rem;"/>
</InputGroupText>
<Button class="flex-grow-1" color="dark" outline disabled>
Fetching
</Button>
{:else if healthWarn}
{#if metricHealth == "failed"}
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
<Icon name="exclamation-circle" style="padding-right: 0.5rem;"/>
<span>Info</span>
@@ -101,13 +89,17 @@
<Button class="flex-grow-1" color="danger" disabled>
No Metrics
</Button>
{:else if metricWarn}
{:else if metricHealth == "partial" || metricHealth == "unknown"}
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
<Icon name="info-circle" style="padding-right: 0.5rem;"/>
<span>Info</span>
</InputGroupText>
<Button class="flex-grow-1" color="warning" disabled>
Missing Metric
{#if metricHealth == "partial"}
Missing Metric(s)
{:else if metricHealth == "unknown"}
Metric Health Unknown
{/if}
</Button>
{:else if nodeJobsData.jobs.count == 1 && nodeJobsData?.jobs?.items[0]?.shared == "none"}
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
@@ -150,8 +142,8 @@
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
State
</InputGroupText>
<Button class="flex-grow-1" color={stateColors[hoststate]} disabled>
{hoststate.charAt(0).toUpperCase() + hoststate.slice(1)}
<Button class="flex-grow-1" color={stateColors[nodeState]} disabled>
{nodeState.charAt(0).toUpperCase() + nodeState.slice(1)}
</Button>
</InputGroup>
</Col>

View File

@@ -75,7 +75,6 @@
const extendedLegendData = $derived($nodeJobsData?.data ? buildExtendedLegend() : null);
const refinedData = $derived(nodeData?.metrics ? sortAndSelectScope(selectedMetrics, nodeData.metrics) : []);
const dataHealth = $derived(refinedData.filter((rd) => rd.availability == "configured").map((enabled) => (nodeDataFetching ? 'fetching' : enabled?.data?.metric?.series?.length > 0)));
/* Functions */
function sortAndSelectScope(metricList = [], nodeMetrics = []) {
@@ -145,11 +144,12 @@
{:else}
<NodeInfo
{cluster}
{dataHealth}
nodeJobsData={$nodeJobsData.data}
subCluster={nodeData.subCluster}
hostname={nodeData.host}
hoststate={nodeData?.state? nodeData.state: 'notindb'}/>
nodeState={nodeData?.nodeState || 'notindb'}
metricHealth={nodeData?.metricHealth || 'unknown'}
/>
{/if}
</td>
{#each refinedData as metricData, i (metricData?.data?.name || i)}