Merge pull request #529 from ClusterCockpit/hotfix

Hotfix
This commit is contained in:
Jan Eitzinger
2026-03-20 05:50:18 +01:00
committed by GitHub
21 changed files with 249 additions and 163 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!]!
} }
@@ -451,8 +452,6 @@ input JobFilter {
duration: IntRange duration: IntRange
energy: FloatRange energy: FloatRange
minRunningFor: Int
numNodes: IntRange numNodes: IntRange
numAccelerators: IntRange numAccelerators: IntRange
numHWThreads: IntRange numHWThreads: IntRange

View File

@@ -289,8 +289,9 @@ type ComplexityRoot struct {
NodeMetrics struct { NodeMetrics struct {
Host func(childComplexity int) int Host func(childComplexity int) int
MetricHealth func(childComplexity int) int
Metrics func(childComplexity int) int Metrics func(childComplexity int) int
State func(childComplexity int) int NodeState func(childComplexity int) int
SubCluster func(childComplexity int) int SubCluster func(childComplexity int) int
} }
@@ -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!]!
} }
@@ -2718,8 +2726,6 @@ input JobFilter {
duration: IntRange duration: IntRange
energy: FloatRange energy: FloatRange
minRunningFor: Int
numNodes: IntRange numNodes: IntRange
numAccelerators: IntRange numAccelerators: IntRange
numHWThreads: IntRange numHWThreads: IntRange
@@ -8318,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,
@@ -8334,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,
@@ -8668,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":
@@ -9846,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":
@@ -13292,7 +13331,7 @@ func (ec *executionContext) unmarshalInputJobFilter(ctx context.Context, obj any
asMap[k] = v asMap[k] = v
} }
fieldsInOrder := [...]string{"tags", "dbId", "jobId", "arrayJobId", "user", "project", "jobName", "cluster", "subCluster", "partition", "duration", "energy", "minRunningFor", "numNodes", "numAccelerators", "numHWThreads", "startTime", "state", "metricStats", "shared", "schedule", "node"} fieldsInOrder := [...]string{"tags", "dbId", "jobId", "arrayJobId", "user", "project", "jobName", "cluster", "subCluster", "partition", "duration", "energy", "numNodes", "numAccelerators", "numHWThreads", "startTime", "state", "metricStats", "shared", "schedule", "node"}
for _, k := range fieldsInOrder { for _, k := range fieldsInOrder {
v, ok := asMap[k] v, ok := asMap[k]
if !ok { if !ok {
@@ -13383,13 +13422,6 @@ func (ec *executionContext) unmarshalInputJobFilter(ctx context.Context, obj any
return it, err return it, err
} }
it.Energy = data it.Energy = data
case "minRunningFor":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("minRunningFor"))
data, err := ec.unmarshalOInt2ᚖint(ctx, v)
if err != nil {
return it, err
}
it.MinRunningFor = data
case "numNodes": case "numNodes":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("numNodes")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("numNodes"))
data, err := ec.unmarshalOIntRange2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋconfigᚐIntRange(ctx, v) data, err := ec.unmarshalOIntRange2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋconfigᚐIntRange(ctx, v)
@@ -15926,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

@@ -75,7 +75,6 @@ type JobFilter struct {
Partition *StringInput `json:"partition,omitempty"` Partition *StringInput `json:"partition,omitempty"`
Duration *config.IntRange `json:"duration,omitempty"` Duration *config.IntRange `json:"duration,omitempty"`
Energy *FloatRange `json:"energy,omitempty"` Energy *FloatRange `json:"energy,omitempty"`
MinRunningFor *int `json:"minRunningFor,omitempty"`
NumNodes *config.IntRange `json:"numNodes,omitempty"` NumNodes *config.IntRange `json:"numNodes,omitempty"`
NumAccelerators *config.IntRange `json:"numAccelerators,omitempty"` NumAccelerators *config.IntRange `json:"numAccelerators,omitempty"`
NumHWThreads *config.IntRange `json:"numHWThreads,omitempty"` NumHWThreads *config.IntRange `json:"numHWThreads,omitempty"`
@@ -195,7 +194,8 @@ 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"`
MetricHealth string `json:"metricHealth"`
SubCluster string `json:"subCluster"` SubCluster string `json:"subCluster"`
Metrics []*JobMetricWithName `json:"metrics"` Metrics []*JobMetricWithName `json:"metrics"`
} }

View File

@@ -676,6 +676,11 @@ func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobF
// Use request-scoped cache: multiple aliases with same (filter, groupBy) // Use request-scoped cache: multiple aliases with same (filter, groupBy)
// but different sortBy/page hit the DB only once. // but different sortBy/page hit the DB only once.
if cache := getStatsGroupCache(ctx); cache != nil { if cache := getStatsGroupCache(ctx); cache != nil {
// Ensure the sort field is computed even if not in the GraphQL selection,
// because sortAndPageStats will sort by it in memory.
if sortBy != nil {
reqFields[sortByFieldName(*sortBy)] = true
}
key := statsCacheKey(filter, groupBy, reqFields) key := statsCacheKey(filter, groupBy, reqFields)
var allStats []*model.JobsStatistics var allStats []*model.JobsStatistics
allStats, err = cache.getOrCompute(key, func() ([]*model.JobsStatistics, error) { allStats, err = cache.getOrCompute(key, func() ([]*model.JobsStatistics, error) {
@@ -840,13 +845,14 @@ 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],
MetricHealth: metricHealthMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)), Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
} }
host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname) host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
@@ -889,7 +895,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")
} }
@@ -911,7 +917,8 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub
for _, hostname := range nodes { for _, hostname := range nodes {
host := &model.NodeMetrics{ host := &model.NodeMetrics{
Host: hostname, Host: hostname,
State: stateMap[hostname], NodeState: nodeStateMap[hostname],
MetricHealth: metricHealthMap[hostname],
Metrics: make([]*model.JobMetricWithName, 0), Metrics: make([]*model.JobMetricWithName, 0),
} }
host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname) host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)

View File

@@ -107,6 +107,33 @@ func sortAndPageStats(allStats []*model.JobsStatistics, sortBy *model.SortByAggr
return sorted return sorted
} }
// sortByFieldName maps a SortByAggregate enum to the corresponding reqFields key.
// This ensures the DB computes the column that sortAndPageStats will sort by.
func sortByFieldName(sortBy model.SortByAggregate) string {
switch sortBy {
case model.SortByAggregateTotaljobs:
return "totalJobs"
case model.SortByAggregateTotalusers:
return "totalUsers"
case model.SortByAggregateTotalwalltime:
return "totalWalltime"
case model.SortByAggregateTotalnodes:
return "totalNodes"
case model.SortByAggregateTotalnodehours:
return "totalNodeHours"
case model.SortByAggregateTotalcores:
return "totalCores"
case model.SortByAggregateTotalcorehours:
return "totalCoreHours"
case model.SortByAggregateTotalaccs:
return "totalAccs"
case model.SortByAggregateTotalacchours:
return "totalAccHours"
default:
return "totalJobs"
}
}
// statsFieldGetter returns a function that extracts the sortable int field // statsFieldGetter returns a function that extracts the sortable int field
// from a JobsStatistics struct for the given sort key. // from a JobsStatistics struct for the given sort key.
func statsFieldGetter(sortBy model.SortByAggregate) func(*model.JobsStatistics) int { func statsFieldGetter(sortBy model.SortByAggregate) func(*model.JobsStatistics) int {

View File

@@ -192,10 +192,10 @@ func LoadData(job *schema.Job,
// Generate statistics series for jobs with many nodes to enable min/median/max graphs // Generate statistics series for jobs with many nodes to enable min/median/max graphs
// instead of overwhelming the UI with individual node lines. Note that newly calculated // instead of overwhelming the UI with individual node lines. Note that newly calculated
// statistics use min/median/max, while archived statistics may use min/mean/max. // statistics use min/median/max, while archived statistics may use min/mean/max.
const maxSeriesSize int = 15 const maxSeriesSize int = 8
for _, scopes := range jd { for _, scopes := range jd {
for _, jm := range scopes { for _, jm := range scopes {
if jm.StatisticsSeries != nil || len(jm.Series) <= maxSeriesSize { if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
continue continue
} }

View File

@@ -275,13 +275,6 @@ func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.Select
} }
} }
// Configurable Filter to exclude recently started jobs, see config.go: ShortRunningJobsDuration
if filter.MinRunningFor != nil {
now := time.Now().Unix()
// Only jobs whose start timestamp is more than MinRunningFor seconds in the past
// If a job completed within the configured timeframe, it will still show up after the start_time matches the condition!
query = query.Where(sq.Lt{"job.start_time": (now - int64(*filter.MinRunningFor))})
}
return query return query
} }

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

@@ -1,12 +1,12 @@
{ {
"name": "cc-frontend", "name": "cc-frontend",
"version": "1.5.0", "version": "1.5.2",
"lockfileVersion": 3, "lockfileVersion": 4,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cc-frontend", "name": "cc-frontend",
"version": "1.5.0", "version": "1.5.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-replace": "^6.0.3",
@@ -27,7 +27,7 @@
"rollup": "^4.59.0", "rollup": "^4.59.0",
"rollup-plugin-css-only": "^4.5.5", "rollup-plugin-css-only": "^4.5.5",
"rollup-plugin-svelte": "^7.2.3", "rollup-plugin-svelte": "^7.2.3",
"svelte": "^5.53.9" "svelte": "^5.54.0"
} }
}, },
"node_modules/@0no-co/graphql.web": { "node_modules/@0no-co/graphql.web": {
@@ -45,9 +45,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.6", "version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -654,6 +654,19 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/types": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz",
"integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==",
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@urql/core": { "node_modules/@urql/core": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz",
@@ -790,9 +803,9 @@
} }
}, },
"node_modules/devalue": { "node_modules/devalue": {
"version": "5.6.3", "version": "5.6.4",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/escape-latex": { "node_modules/escape-latex": {
@@ -808,12 +821,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/esrap": { "node_modules/esrap": {
"version": "2.2.3", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz",
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==", "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15",
"@typescript-eslint/types": "^8.2.0"
} }
}, },
"node_modules/estree-walker": { "node_modules/estree-walker": {
@@ -1193,9 +1207,9 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.53.9", "version": "5.54.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.9.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.0.tgz",
"integrity": "sha512-MwDfWsN8qZzeP0jlQsWF4k/4B3csb3IbzCRggF+L/QqY7T8bbKvnChEo1cPZztF51HJQhilDbevWYl2LvXbquA==", "integrity": "sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.4", "@jridgewell/remapping": "^2.3.4",
@@ -1207,7 +1221,7 @@
"aria-query": "5.3.1", "aria-query": "5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"devalue": "^5.6.3", "devalue": "^5.6.4",
"esm-env": "^1.2.1", "esm-env": "^1.2.1",
"esrap": "^2.2.2", "esrap": "^2.2.2",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
@@ -1229,9 +1243,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.46.0", "version": "5.46.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "cc-frontend", "name": "cc-frontend",
"version": "1.5.0", "version": "1.5.2",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",
@@ -14,7 +14,7 @@
"rollup": "^4.59.0", "rollup": "^4.59.0",
"rollup-plugin-css-only": "^4.5.5", "rollup-plugin-css-only": "^4.5.5",
"rollup-plugin-svelte": "^7.2.3", "rollup-plugin-svelte": "^7.2.3",
"svelte": "^5.53.9" "svelte": "^5.54.0"
}, },
"dependencies": { "dependencies": {
"@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-replace": "^6.0.3",

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

@@ -229,7 +229,7 @@
timestep={metric.data.metric.timestep} timestep={metric.data.metric.timestep}
scope={metric.data.scope} scope={metric.data.scope}
series={metric.data.metric.series} series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries} statisticsSeries={metric.data.metric?.statisticsSeries}
metric={metric.data.name} metric={metric.data.name}
cluster={clusterInfos.find((c) => c.name == job.cluster)} cluster={clusterInfos.find((c) => c.name == job.cluster)}
subCluster={job.subCluster} subCluster={job.subCluster}

View File

@@ -236,7 +236,7 @@
// conditional hide series color markers: // conditional hide series color markers:
if ( if (
// useStatsSeries || // Min/Max/Median Self-Explanatory // Min/Max/Median Self-Explanatory
dataSize === 1 || // Only one Y-Dataseries dataSize === 1 || // Only one Y-Dataseries
dataSize > 8 // More than 8 Y-Dataseries dataSize > 8 // More than 8 Y-Dataseries
) { ) {
@@ -273,7 +273,7 @@
} }
} }
if (dataSize <= 12 ) { // || useStatsSeries) { if (dataSize <= 12 ) {
return { return {
hooks: { hooks: {
init: init, init: init,

View File

@@ -9,7 +9,6 @@
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `timestep Number`: The timestep used for X-axis rendering - `timestep Number`: The timestep used for X-axis rendering
- `series [GraphQL.Series]`: The metric data object - `series [GraphQL.Series]`: The metric data object
- `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: false]
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null] - `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
- `cluster String?`: Cluster name of the parent job / data [Default: ""] - `cluster String?`: Cluster name of the parent job / data [Default: ""]
- `subCluster String`: Name of the subCluster of the parent job - `subCluster String`: Name of the subCluster of the parent job
@@ -37,7 +36,6 @@
height = 300, height = 300,
timestep, timestep,
series, series,
useStatsSeries = false,
statisticsSeries = null, statisticsSeries = null,
cluster = "", cluster = "",
subCluster, subCluster,
@@ -78,6 +76,7 @@
const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null); const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null);
const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null); const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null);
const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null); const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null);
const useStatsSeries = $derived(!!statisticsSeries); // Display Stats Series By Default if Exists
const thresholds = $derived(findJobAggregationThresholds( const thresholds = $derived(findJobAggregationThresholds(
subClusterTopology, subClusterTopology,
metricConfig, metricConfig,
@@ -243,11 +242,6 @@
return pendingSeries; return pendingSeries;
}) })
/* Effects */
$effect(() => {
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
})
// This updates plot on all size changes if wrapper (== data) exists // This updates plot on all size changes if wrapper (== data) exists
$effect(() => { $effect(() => {
if (plotWrapper) { if (plotWrapper) {

View File

@@ -197,7 +197,6 @@
{zoomState} {zoomState}
{thresholdState} {thresholdState}
statisticsSeries={statsSeries[selectedScopeIndex]} statisticsSeries={statsSeries[selectedScopeIndex]}
useStatsSeries={!!statsSeries[selectedScopeIndex]}
enableFlip enableFlip
/> />
{/if} {/if}

View File

@@ -7,7 +7,7 @@
- `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 hostname filter [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

@@ -6,7 +6,7 @@
- `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 hostname filter [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)}
@@ -174,7 +174,7 @@
<p>No dataset(s) returned for <b>{selectedMetrics[i]}</b></p> <p>No dataset(s) returned for <b>{selectedMetrics[i]}</b></p>
<p class="mb-1">Metric or host was not found in metric store for cluster <b>{cluster}</b>.</p> <p class="mb-1">Metric or host was not found in metric store for cluster <b>{cluster}</b>.</p>
</Card> </Card>
{:else if !!metricData.data?.metric.statisticsSeries} {:else if !!metricData?.data?.metric?.statisticsSeries}
<!-- "No Data"-Warning included in MetricPlot-Component --> <!-- "No Data"-Warning included in MetricPlot-Component -->
<MetricPlot <MetricPlot
{cluster} {cluster}
@@ -183,8 +183,7 @@
scope={metricData.data.scope} scope={metricData.data.scope}
timestep={metricData.data.metric.timestep} timestep={metricData.data.metric.timestep}
series={metricData.data.metric.series} series={metricData.data.metric.series}
statisticsSeries={metricData.data?.metric.statisticsSeries} statisticsSeries={metricData.data.metric.statisticsSeries}
useStatsSeries={!!metricData.data?.metric.statisticsSeries}
height={175} height={175}
{plotSync} {plotSync}
forNode forNode