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

@@ -270,7 +270,8 @@ enum SortByAggregate {
type NodeMetrics { type NodeMetrics {
host: String! host: String!
state: String! nodeState: String!
metricHealth: String!
subCluster: String! subCluster: String!
metrics: [JobMetricWithName!]! metrics: [JobMetricWithName!]!
} }

View File

@@ -288,10 +288,11 @@ type ComplexityRoot struct {
} }
NodeMetrics struct { NodeMetrics struct {
Host func(childComplexity int) int Host func(childComplexity int) int
Metrics func(childComplexity int) int MetricHealth func(childComplexity int) int
State func(childComplexity int) int Metrics func(childComplexity int) int
SubCluster func(childComplexity int) int NodeState func(childComplexity int) int
SubCluster func(childComplexity int) int
} }
NodeStateResultList struct { NodeStateResultList struct {
@@ -1501,18 +1502,24 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
} }
return e.ComplexityRoot.NodeMetrics.Host(childComplexity), true return e.ComplexityRoot.NodeMetrics.Host(childComplexity), true
case "NodeMetrics.metricHealth":
if e.ComplexityRoot.NodeMetrics.MetricHealth == nil {
break
}
return e.ComplexityRoot.NodeMetrics.MetricHealth(childComplexity), true
case "NodeMetrics.metrics": case "NodeMetrics.metrics":
if e.ComplexityRoot.NodeMetrics.Metrics == nil { if e.ComplexityRoot.NodeMetrics.Metrics == nil {
break break
} }
return e.ComplexityRoot.NodeMetrics.Metrics(childComplexity), true return e.ComplexityRoot.NodeMetrics.Metrics(childComplexity), true
case "NodeMetrics.state": case "NodeMetrics.nodeState":
if e.ComplexityRoot.NodeMetrics.State == nil { if e.ComplexityRoot.NodeMetrics.NodeState == nil {
break break
} }
return e.ComplexityRoot.NodeMetrics.State(childComplexity), true return e.ComplexityRoot.NodeMetrics.NodeState(childComplexity), true
case "NodeMetrics.subCluster": case "NodeMetrics.subCluster":
if e.ComplexityRoot.NodeMetrics.SubCluster == nil { if e.ComplexityRoot.NodeMetrics.SubCluster == nil {
break break
@@ -2537,7 +2544,8 @@ enum SortByAggregate {
type NodeMetrics { type NodeMetrics {
host: String! host: String!
state: String! nodeState: String!
metricHealth: String!
subCluster: String! subCluster: String!
metrics: [JobMetricWithName!]! metrics: [JobMetricWithName!]!
} }
@@ -8316,14 +8324,14 @@ func (ec *executionContext) fieldContext_NodeMetrics_host(_ context.Context, fie
return fc, nil return fc, nil
} }
func (ec *executionContext) _NodeMetrics_state(ctx context.Context, field graphql.CollectedField, obj *model.NodeMetrics) (ret graphql.Marshaler) { func (ec *executionContext) _NodeMetrics_nodeState(ctx context.Context, field graphql.CollectedField, obj *model.NodeMetrics) (ret graphql.Marshaler) {
return graphql.ResolveField( return graphql.ResolveField(
ctx, ctx,
ec.OperationContext, ec.OperationContext,
field, field,
ec.fieldContext_NodeMetrics_state, ec.fieldContext_NodeMetrics_nodeState,
func(ctx context.Context) (any, error) { func(ctx context.Context) (any, error) {
return obj.State, nil return obj.NodeState, nil
}, },
nil, nil,
ec.marshalNString2string, ec.marshalNString2string,
@@ -8332,7 +8340,36 @@ func (ec *executionContext) _NodeMetrics_state(ctx context.Context, field graphq
) )
} }
func (ec *executionContext) fieldContext_NodeMetrics_state(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { func (ec *executionContext) fieldContext_NodeMetrics_nodeState(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "NodeMetrics",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _NodeMetrics_metricHealth(ctx context.Context, field graphql.CollectedField, obj *model.NodeMetrics) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_NodeMetrics_metricHealth,
func(ctx context.Context) (any, error) {
return obj.MetricHealth, nil
},
nil,
ec.marshalNString2string,
true,
true,
)
}
func (ec *executionContext) fieldContext_NodeMetrics_metricHealth(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{ fc = &graphql.FieldContext{
Object: "NodeMetrics", Object: "NodeMetrics",
Field: field, Field: field,
@@ -8666,8 +8703,10 @@ func (ec *executionContext) fieldContext_NodesResultList_items(_ context.Context
switch field.Name { switch field.Name {
case "host": case "host":
return ec.fieldContext_NodeMetrics_host(ctx, field) return ec.fieldContext_NodeMetrics_host(ctx, field)
case "state": case "nodeState":
return ec.fieldContext_NodeMetrics_state(ctx, field) return ec.fieldContext_NodeMetrics_nodeState(ctx, field)
case "metricHealth":
return ec.fieldContext_NodeMetrics_metricHealth(ctx, field)
case "subCluster": case "subCluster":
return ec.fieldContext_NodeMetrics_subCluster(ctx, field) return ec.fieldContext_NodeMetrics_subCluster(ctx, field)
case "metrics": case "metrics":
@@ -9844,8 +9883,10 @@ func (ec *executionContext) fieldContext_Query_nodeMetrics(ctx context.Context,
switch field.Name { switch field.Name {
case "host": case "host":
return ec.fieldContext_NodeMetrics_host(ctx, field) return ec.fieldContext_NodeMetrics_host(ctx, field)
case "state": case "nodeState":
return ec.fieldContext_NodeMetrics_state(ctx, field) return ec.fieldContext_NodeMetrics_nodeState(ctx, field)
case "metricHealth":
return ec.fieldContext_NodeMetrics_metricHealth(ctx, field)
case "subCluster": case "subCluster":
return ec.fieldContext_NodeMetrics_subCluster(ctx, field) return ec.fieldContext_NodeMetrics_subCluster(ctx, field)
case "metrics": case "metrics":
@@ -15917,8 +15958,13 @@ func (ec *executionContext) _NodeMetrics(ctx context.Context, sel ast.SelectionS
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "state": case "nodeState":
out.Values[i] = ec._NodeMetrics_state(ctx, field, obj) out.Values[i] = ec._NodeMetrics_nodeState(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "metricHealth":
out.Values[i] = ec._NodeMetrics_metricHealth(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }

View File

@@ -193,10 +193,11 @@ type NodeFilter struct {
} }
type NodeMetrics struct { type NodeMetrics struct {
Host string `json:"host"` Host string `json:"host"`
State string `json:"state"` NodeState string `json:"nodeState"`
SubCluster string `json:"subCluster"` MetricHealth string `json:"metricHealth"`
Metrics []*JobMetricWithName `json:"metrics"` SubCluster string `json:"subCluster"`
Metrics []*JobMetricWithName `json:"metrics"`
} }
type NodeStateResultList struct { type NodeStateResultList struct {

View File

@@ -840,14 +840,15 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
} }
nodeRepo := repository.GetNodeRepository() nodeRepo := repository.GetNodeRepository()
stateMap, _ := nodeRepo.MapNodes(cluster) nodeStateMap, metricHealthMap, _ := nodeRepo.MapNodes(cluster)
nodeMetrics := make([]*model.NodeMetrics, 0, len(data)) nodeMetrics := make([]*model.NodeMetrics, 0, len(data))
for hostname, metrics := range data { for hostname, metrics := range data {
host := &model.NodeMetrics{ host := &model.NodeMetrics{
Host: hostname, Host: hostname,
State: stateMap[hostname], NodeState: nodeStateMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)), MetricHealth: metricHealthMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
} }
host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname) host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
if err != nil { if err != nil {
@@ -889,7 +890,7 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub
nodeRepo := repository.GetNodeRepository() nodeRepo := repository.GetNodeRepository()
// nodes -> array hostname // nodes -> array hostname
nodes, stateMap, countNodes, hasNextPage, nerr := nodeRepo.GetNodesForList(ctx, cluster, subCluster, stateFilter, nodeFilter, page) nodes, nodeStateMap, metricHealthMap, countNodes, hasNextPage, nerr := nodeRepo.GetNodesForList(ctx, cluster, subCluster, stateFilter, nodeFilter, page)
if nerr != nil { if nerr != nil {
return nil, errors.New("could not retrieve node list required for resolving NodeMetricsList") return nil, errors.New("could not retrieve node list required for resolving NodeMetricsList")
} }
@@ -910,9 +911,10 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub
nodeMetricsList := make([]*model.NodeMetrics, 0, len(data)) nodeMetricsList := make([]*model.NodeMetrics, 0, len(data))
for _, hostname := range nodes { for _, hostname := range nodes {
host := &model.NodeMetrics{ host := &model.NodeMetrics{
Host: hostname, Host: hostname,
State: stateMap[hostname], NodeState: nodeStateMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0), MetricHealth: metricHealthMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0),
} }
host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname) host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
if err != nil { if err != nil {

View File

@@ -593,8 +593,8 @@ func (r *NodeRepository) ListNodes(cluster string) ([]*schema.Node, error) {
return nodeList, nil return nodeList, nil
} }
func (r *NodeRepository) MapNodes(cluster string) (map[string]string, error) { func (r *NodeRepository) MapNodes(cluster string) (map[string]string, map[string]string, error) {
q := sq.Select("node.hostname", "node_state.node_state"). q := sq.Select("node.hostname", "node_state.node_state", "node_state.health_state").
From("node"). From("node").
Join("node_state ON node_state.node_id = node.id"). Join("node_state ON node_state.node_id = node.id").
Where(latestStateCondition()). Where(latestStateCondition()).
@@ -604,22 +604,25 @@ func (r *NodeRepository) MapNodes(cluster string) (map[string]string, error) {
rows, err := q.RunWith(r.DB).Query() rows, err := q.RunWith(r.DB).Query()
if err != nil { if err != nil {
cclog.Warn("Error while querying node list") cclog.Warn("Error while querying node list")
return nil, err return nil, nil, err
} }
stateMap := make(map[string]string) nodeStateMap := make(map[string]string)
metricHealthMap := make(map[string]string)
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var hostname, nodestate string var hostname, nodeState, metricHealth string
if err := rows.Scan(&hostname, &nodestate); err != nil { if err := rows.Scan(&hostname, &nodeState, &metricHealth); err != nil {
cclog.Warn("Error while scanning node list (MapNodes)") cclog.Warn("Error while scanning node list (MapNodes)")
return nil, err return nil, nil, err
} }
stateMap[hostname] = nodestate nodeStateMap[hostname] = nodeState
metricHealthMap[hostname] = metricHealth
} }
return stateMap, nil return nodeStateMap, metricHealthMap, nil
} }
func (r *NodeRepository) CountStates(ctx context.Context, filters []*model.NodeFilter, column string) ([]*model.NodeStates, error) { func (r *NodeRepository) CountStates(ctx context.Context, filters []*model.NodeFilter, column string) ([]*model.NodeStates, error) {
@@ -741,10 +744,11 @@ func (r *NodeRepository) GetNodesForList(
stateFilter string, stateFilter string,
nodeFilter string, nodeFilter string,
page *model.PageRequest, page *model.PageRequest,
) ([]string, map[string]string, int, bool, error) { ) ([]string, map[string]string, map[string]string, int, bool, error) {
// Init Return Vars // Init Return Vars
nodes := make([]string, 0) nodes := make([]string, 0)
stateMap := make(map[string]string) nodeStateMap := make(map[string]string)
metricHealthMap := make(map[string]string)
countNodes := 0 countNodes := 0
hasNextPage := false hasNextPage := false
@@ -778,7 +782,7 @@ func (r *NodeRepository) GetNodesForList(
rawNodes, serr := r.QueryNodes(ctx, queryFilters, page, nil) // Order not Used rawNodes, serr := r.QueryNodes(ctx, queryFilters, page, nil) // Order not Used
if serr != nil { if serr != nil {
cclog.Warn("error while loading node database data (Resolver.NodeMetricsList)") cclog.Warn("error while loading node database data (Resolver.NodeMetricsList)")
return nil, nil, 0, false, serr return nil, nil, nil, 0, false, serr
} }
// Intermediate Node Result Info // Intermediate Node Result Info
@@ -787,7 +791,8 @@ func (r *NodeRepository) GetNodesForList(
continue continue
} }
nodes = append(nodes, node.Hostname) nodes = append(nodes, node.Hostname)
stateMap[node.Hostname] = string(node.NodeState) nodeStateMap[node.Hostname] = string(node.NodeState)
metricHealthMap[node.Hostname] = string(node.HealthState)
} }
// Special Case: Find Nodes not in DB node table but in metricStore only // Special Case: Find Nodes not in DB node table but in metricStore only
@@ -847,7 +852,7 @@ func (r *NodeRepository) GetNodesForList(
countNodes, cerr = r.CountNodes(ctx, queryFilters) countNodes, cerr = r.CountNodes(ctx, queryFilters)
if cerr != nil { if cerr != nil {
cclog.Warn("error while counting node database data (Resolver.NodeMetricsList)") cclog.Warn("error while counting node database data (Resolver.NodeMetricsList)")
return nil, nil, 0, false, cerr return nil, nil, nil, 0, false, cerr
} }
hasNextPage = page.Page*page.ItemsPerPage < countNodes hasNextPage = page.Page*page.ItemsPerPage < countNodes
} }
@@ -857,7 +862,7 @@ func (r *NodeRepository) GetNodesForList(
nodes, countNodes, hasNextPage = getNodesFromTopol(cluster, subCluster, nodeFilter, page) nodes, countNodes, hasNextPage = getNodesFromTopol(cluster, subCluster, nodeFilter, page)
} }
return nodes, stateMap, countNodes, hasNextPage, nil return nodes, nodeStateMap, metricHealthMap, countNodes, hasNextPage, nil
} }
func AccessCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) { func AccessCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,8 @@
- `cluster String`: The nodes' cluster - `cluster String`: The nodes' cluster
- `subCluster String`: The nodes' subCluster - `subCluster String`: The nodes' subCluster
- `hostname String`: The nodes' hostname - `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] - `nodeJobsData [Object]`: Data returned by GQL for jobs runninig on this node [Default: null]
--> -->
@@ -32,8 +33,8 @@
cluster, cluster,
subCluster, subCluster,
hostname, hostname,
hoststate, nodeState,
dataHealth, metricHealth,
nodeJobsData = null, nodeJobsData = null,
} = $props(); } = $props();
@@ -50,12 +51,6 @@
} }
/* Derived */ /* 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 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)) ? 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}}> <Row cols={{xs: 1, lg: 2}}>
<Col class="mb-2 mb-lg-0"> <Col class="mb-2 mb-lg-0">
<InputGroup size="sm"> <InputGroup size="sm">
{#if fetchInfo} {#if metricHealth == "failed"}
<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}
<InputGroupText class="flex-grow-1 flex-lg-grow-0"> <InputGroupText class="flex-grow-1 flex-lg-grow-0">
<Icon name="exclamation-circle" style="padding-right: 0.5rem;"/> <Icon name="exclamation-circle" style="padding-right: 0.5rem;"/>
<span>Info</span> <span>Info</span>
@@ -101,13 +89,17 @@
<Button class="flex-grow-1" color="danger" disabled> <Button class="flex-grow-1" color="danger" disabled>
No Metrics No Metrics
</Button> </Button>
{:else if metricWarn} {:else if metricHealth == "partial" || metricHealth == "unknown"}
<InputGroupText class="flex-grow-1 flex-lg-grow-0"> <InputGroupText class="flex-grow-1 flex-lg-grow-0">
<Icon name="info-circle" style="padding-right: 0.5rem;"/> <Icon name="info-circle" style="padding-right: 0.5rem;"/>
<span>Info</span> <span>Info</span>
</InputGroupText> </InputGroupText>
<Button class="flex-grow-1" color="warning" disabled> <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> </Button>
{:else if nodeJobsData.jobs.count == 1 && nodeJobsData?.jobs?.items[0]?.shared == "none"} {:else if nodeJobsData.jobs.count == 1 && nodeJobsData?.jobs?.items[0]?.shared == "none"}
<InputGroupText class="flex-grow-1 flex-lg-grow-0"> <InputGroupText class="flex-grow-1 flex-lg-grow-0">
@@ -150,8 +142,8 @@
<InputGroupText class="flex-grow-1 flex-lg-grow-0"> <InputGroupText class="flex-grow-1 flex-lg-grow-0">
State State
</InputGroupText> </InputGroupText>
<Button class="flex-grow-1" color={stateColors[hoststate]} disabled> <Button class="flex-grow-1" color={stateColors[nodeState]} disabled>
{hoststate.charAt(0).toUpperCase() + hoststate.slice(1)} {nodeState.charAt(0).toUpperCase() + nodeState.slice(1)}
</Button> </Button>
</InputGroup> </InputGroup>
</Col> </Col>

View File

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