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 {
host: String!
state: String!
nodeState: String!
metricHealth: String!
subCluster: String!
metrics: [JobMetricWithName!]!
}

View File

@@ -288,10 +288,11 @@ type ComplexityRoot struct {
}
NodeMetrics struct {
Host func(childComplexity int) int
Metrics func(childComplexity int) int
State func(childComplexity int) int
SubCluster func(childComplexity int) int
Host func(childComplexity int) int
MetricHealth func(childComplexity int) int
Metrics func(childComplexity int) int
NodeState func(childComplexity int) int
SubCluster func(childComplexity int) int
}
NodeStateResultList struct {
@@ -1501,18 +1502,24 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
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":
if e.ComplexityRoot.NodeMetrics.Metrics == nil {
break
}
return e.ComplexityRoot.NodeMetrics.Metrics(childComplexity), true
case "NodeMetrics.state":
if e.ComplexityRoot.NodeMetrics.State == nil {
case "NodeMetrics.nodeState":
if e.ComplexityRoot.NodeMetrics.NodeState == nil {
break
}
return e.ComplexityRoot.NodeMetrics.State(childComplexity), true
return e.ComplexityRoot.NodeMetrics.NodeState(childComplexity), true
case "NodeMetrics.subCluster":
if e.ComplexityRoot.NodeMetrics.SubCluster == nil {
break
@@ -2537,7 +2544,8 @@ enum SortByAggregate {
type NodeMetrics {
host: String!
state: String!
nodeState: String!
metricHealth: String!
subCluster: String!
metrics: [JobMetricWithName!]!
}
@@ -8316,14 +8324,14 @@ func (ec *executionContext) fieldContext_NodeMetrics_host(_ context.Context, fie
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(
ctx,
ec.OperationContext,
field,
ec.fieldContext_NodeMetrics_state,
ec.fieldContext_NodeMetrics_nodeState,
func(ctx context.Context) (any, error) {
return obj.State, nil
return obj.NodeState, nil
},
nil,
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{
Object: "NodeMetrics",
Field: field,
@@ -8666,8 +8703,10 @@ func (ec *executionContext) fieldContext_NodesResultList_items(_ context.Context
switch field.Name {
case "host":
return ec.fieldContext_NodeMetrics_host(ctx, field)
case "state":
return ec.fieldContext_NodeMetrics_state(ctx, field)
case "nodeState":
return ec.fieldContext_NodeMetrics_nodeState(ctx, field)
case "metricHealth":
return ec.fieldContext_NodeMetrics_metricHealth(ctx, field)
case "subCluster":
return ec.fieldContext_NodeMetrics_subCluster(ctx, field)
case "metrics":
@@ -9844,8 +9883,10 @@ func (ec *executionContext) fieldContext_Query_nodeMetrics(ctx context.Context,
switch field.Name {
case "host":
return ec.fieldContext_NodeMetrics_host(ctx, field)
case "state":
return ec.fieldContext_NodeMetrics_state(ctx, field)
case "nodeState":
return ec.fieldContext_NodeMetrics_nodeState(ctx, field)
case "metricHealth":
return ec.fieldContext_NodeMetrics_metricHealth(ctx, field)
case "subCluster":
return ec.fieldContext_NodeMetrics_subCluster(ctx, field)
case "metrics":
@@ -15917,8 +15958,13 @@ func (ec *executionContext) _NodeMetrics(ctx context.Context, sel ast.SelectionS
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "state":
out.Values[i] = ec._NodeMetrics_state(ctx, field, obj)
case "nodeState":
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 {
out.Invalids++
}

View File

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

View File

@@ -840,14 +840,15 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
}
nodeRepo := repository.GetNodeRepository()
stateMap, _ := nodeRepo.MapNodes(cluster)
nodeStateMap, metricHealthMap, _ := nodeRepo.MapNodes(cluster)
nodeMetrics := make([]*model.NodeMetrics, 0, len(data))
for hostname, metrics := range data {
host := &model.NodeMetrics{
Host: hostname,
State: stateMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
Host: hostname,
NodeState: nodeStateMap[hostname],
MetricHealth: metricHealthMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
}
host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
if err != nil {
@@ -889,7 +890,7 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub
nodeRepo := repository.GetNodeRepository()
// 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 {
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))
for _, hostname := range nodes {
host := &model.NodeMetrics{
Host: hostname,
State: stateMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0),
Host: hostname,
NodeState: nodeStateMap[hostname],
MetricHealth: metricHealthMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0),
}
host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
if err != nil {

View File

@@ -593,8 +593,8 @@ func (r *NodeRepository) ListNodes(cluster string) ([]*schema.Node, error) {
return nodeList, nil
}
func (r *NodeRepository) MapNodes(cluster string) (map[string]string, error) {
q := sq.Select("node.hostname", "node_state.node_state").
func (r *NodeRepository) MapNodes(cluster string) (map[string]string, map[string]string, error) {
q := sq.Select("node.hostname", "node_state.node_state", "node_state.health_state").
From("node").
Join("node_state ON node_state.node_id = node.id").
Where(latestStateCondition()).
@@ -604,22 +604,25 @@ func (r *NodeRepository) MapNodes(cluster string) (map[string]string, error) {
rows, err := q.RunWith(r.DB).Query()
if err != nil {
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()
for rows.Next() {
var hostname, nodestate string
if err := rows.Scan(&hostname, &nodestate); err != nil {
var hostname, nodeState, metricHealth string
if err := rows.Scan(&hostname, &nodeState, &metricHealth); err != nil {
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) {
@@ -741,10 +744,11 @@ func (r *NodeRepository) GetNodesForList(
stateFilter string,
nodeFilter string,
page *model.PageRequest,
) ([]string, map[string]string, int, bool, error) {
) ([]string, map[string]string, map[string]string, int, bool, error) {
// Init Return Vars
nodes := make([]string, 0)
stateMap := make(map[string]string)
nodeStateMap := make(map[string]string)
metricHealthMap := make(map[string]string)
countNodes := 0
hasNextPage := false
@@ -778,7 +782,7 @@ func (r *NodeRepository) GetNodesForList(
rawNodes, serr := r.QueryNodes(ctx, queryFilters, page, nil) // Order not Used
if serr != nil {
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
@@ -787,7 +791,8 @@ func (r *NodeRepository) GetNodesForList(
continue
}
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
@@ -847,7 +852,7 @@ func (r *NodeRepository) GetNodesForList(
countNodes, cerr = r.CountNodes(ctx, queryFilters)
if cerr != nil {
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
}
@@ -857,7 +862,7 @@ func (r *NodeRepository) GetNodesForList(
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) {

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)}