mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-03-27 10:07:34 +01:00
@@ -270,7 +270,8 @@ enum SortByAggregate {
|
||||
|
||||
type NodeMetrics {
|
||||
host: String!
|
||||
state: String!
|
||||
nodeState: String!
|
||||
metricHealth: String!
|
||||
subCluster: String!
|
||||
metrics: [JobMetricWithName!]!
|
||||
}
|
||||
@@ -451,8 +452,6 @@ input JobFilter {
|
||||
duration: IntRange
|
||||
energy: FloatRange
|
||||
|
||||
minRunningFor: Int
|
||||
|
||||
numNodes: IntRange
|
||||
numAccelerators: IntRange
|
||||
numHWThreads: IntRange
|
||||
|
||||
@@ -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!]!
|
||||
}
|
||||
@@ -2718,8 +2726,6 @@ input JobFilter {
|
||||
duration: IntRange
|
||||
energy: FloatRange
|
||||
|
||||
minRunningFor: Int
|
||||
|
||||
numNodes: IntRange
|
||||
numAccelerators: IntRange
|
||||
numHWThreads: IntRange
|
||||
@@ -8318,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,
|
||||
@@ -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{
|
||||
Object: "NodeMetrics",
|
||||
Field: field,
|
||||
@@ -8668,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":
|
||||
@@ -9846,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":
|
||||
@@ -13292,7 +13331,7 @@ func (ec *executionContext) unmarshalInputJobFilter(ctx context.Context, obj any
|
||||
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 {
|
||||
v, ok := asMap[k]
|
||||
if !ok {
|
||||
@@ -13383,13 +13422,6 @@ func (ec *executionContext) unmarshalInputJobFilter(ctx context.Context, obj any
|
||||
return it, err
|
||||
}
|
||||
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":
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("numNodes"))
|
||||
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 {
|
||||
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++
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@ type JobFilter struct {
|
||||
Partition *StringInput `json:"partition,omitempty"`
|
||||
Duration *config.IntRange `json:"duration,omitempty"`
|
||||
Energy *FloatRange `json:"energy,omitempty"`
|
||||
MinRunningFor *int `json:"minRunningFor,omitempty"`
|
||||
NumNodes *config.IntRange `json:"numNodes,omitempty"`
|
||||
NumAccelerators *config.IntRange `json:"numAccelerators,omitempty"`
|
||||
NumHWThreads *config.IntRange `json:"numHWThreads,omitempty"`
|
||||
@@ -194,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 {
|
||||
|
||||
@@ -676,6 +676,11 @@ func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobF
|
||||
// Use request-scoped cache: multiple aliases with same (filter, groupBy)
|
||||
// but different sortBy/page hit the DB only once.
|
||||
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)
|
||||
var allStats []*model.JobsStatistics
|
||||
allStats, err = cache.getOrCompute(key, func() ([]*model.JobsStatistics, error) {
|
||||
@@ -840,14 +845,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 +895,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 +916,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 {
|
||||
|
||||
@@ -107,6 +107,33 @@ func sortAndPageStats(allStats []*model.JobsStatistics, sortBy *model.SortByAggr
|
||||
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
|
||||
// from a JobsStatistics struct for the given sort key.
|
||||
func statsFieldGetter(sortBy model.SortByAggregate) func(*model.JobsStatistics) int {
|
||||
|
||||
@@ -192,10 +192,10 @@ func LoadData(job *schema.Job,
|
||||
// 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
|
||||
// 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 _, jm := range scopes {
|
||||
if jm.StatisticsSeries != nil || len(jm.Series) <= maxSeriesSize {
|
||||
if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
56
web/frontend/package-lock.json
generated
56
web/frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "cc-frontend",
|
||||
"version": "1.5.0",
|
||||
"lockfileVersion": 3,
|
||||
"version": "1.5.2",
|
||||
"lockfileVersion": 4,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cc-frontend",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
@@ -27,7 +27,7 @@
|
||||
"rollup": "^4.59.0",
|
||||
"rollup-plugin-css-only": "^4.5.5",
|
||||
"rollup-plugin-svelte": "^7.2.3",
|
||||
"svelte": "^5.53.9"
|
||||
"svelte": "^5.54.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@0no-co/graphql.web": {
|
||||
@@ -45,9 +45,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -654,6 +654,19 @@
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"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": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz",
|
||||
@@ -790,9 +803,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
|
||||
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-latex": {
|
||||
@@ -808,12 +821,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz",
|
||||
"integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@typescript-eslint/types": "^8.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
@@ -1193,9 +1207,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.53.9",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.9.tgz",
|
||||
"integrity": "sha512-MwDfWsN8qZzeP0jlQsWF4k/4B3csb3IbzCRggF+L/QqY7T8bbKvnChEo1cPZztF51HJQhilDbevWYl2LvXbquA==",
|
||||
"version": "5.54.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.0.tgz",
|
||||
"integrity": "sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
@@ -1207,7 +1221,7 @@
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.3",
|
||||
"devalue": "^5.6.4",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.2.2",
|
||||
"is-reference": "^3.0.3",
|
||||
@@ -1229,9 +1243,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.46.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
|
||||
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
|
||||
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cc-frontend",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.2",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
@@ -14,7 +14,7 @@
|
||||
"rollup": "^4.59.0",
|
||||
"rollup-plugin-css-only": "^4.5.5",
|
||||
"rollup-plugin-svelte": "^7.2.3",
|
||||
"svelte": "^5.53.9"
|
||||
"svelte": "^5.54.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/plugin-replace": "^6.0.3",
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
name
|
||||
count
|
||||
}
|
||||
# Get Current States fir Pie Charts
|
||||
# Get Current States for Pie Charts
|
||||
nodeStates(filter: $nodeFilter) {
|
||||
state
|
||||
count
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
timestep={metric.data.metric.timestep}
|
||||
scope={metric.data.scope}
|
||||
series={metric.data.metric.series}
|
||||
statisticsSeries={metric.data.metric.statisticsSeries}
|
||||
statisticsSeries={metric.data.metric?.statisticsSeries}
|
||||
metric={metric.data.name}
|
||||
cluster={clusterInfos.find((c) => c.name == job.cluster)}
|
||||
subCluster={job.subCluster}
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
|
||||
// conditional hide series color markers:
|
||||
if (
|
||||
// useStatsSeries || // Min/Max/Median Self-Explanatory
|
||||
// Min/Max/Median Self-Explanatory
|
||||
dataSize === 1 || // Only one Y-Dataseries
|
||||
dataSize > 8 // More than 8 Y-Dataseries
|
||||
) {
|
||||
@@ -273,7 +273,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (dataSize <= 12 ) { // || useStatsSeries) {
|
||||
if (dataSize <= 12 ) {
|
||||
return {
|
||||
hooks: {
|
||||
init: init,
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
- `height Number?`: The plot height [Default: 300]
|
||||
- `timestep Number`: The timestep used for X-axis rendering
|
||||
- `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]
|
||||
- `cluster String?`: Cluster name of the parent job / data [Default: ""]
|
||||
- `subCluster String`: Name of the subCluster of the parent job
|
||||
@@ -37,7 +36,6 @@
|
||||
height = 300,
|
||||
timestep,
|
||||
series,
|
||||
useStatsSeries = false,
|
||||
statisticsSeries = null,
|
||||
cluster = "",
|
||||
subCluster,
|
||||
@@ -78,6 +76,7 @@
|
||||
const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null);
|
||||
const resampleResolutions = $derived(resampleConfig?.resolutions ? [...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(
|
||||
subClusterTopology,
|
||||
metricConfig,
|
||||
@@ -243,11 +242,6 @@
|
||||
return pendingSeries;
|
||||
})
|
||||
|
||||
/* Effects */
|
||||
$effect(() => {
|
||||
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
|
||||
})
|
||||
|
||||
// This updates plot on all size changes if wrapper (== data) exists
|
||||
$effect(() => {
|
||||
if (plotWrapper) {
|
||||
|
||||
@@ -197,7 +197,6 @@
|
||||
{zoomState}
|
||||
{thresholdState}
|
||||
statisticsSeries={statsSeries[selectedScopeIndex]}
|
||||
useStatsSeries={!!statsSeries[selectedScopeIndex]}
|
||||
enableFlip
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
@@ -174,7 +174,7 @@
|
||||
<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>
|
||||
</Card>
|
||||
{:else if !!metricData.data?.metric.statisticsSeries}
|
||||
{:else if !!metricData?.data?.metric?.statisticsSeries}
|
||||
<!-- "No Data"-Warning included in MetricPlot-Component -->
|
||||
<MetricPlot
|
||||
{cluster}
|
||||
@@ -183,8 +183,7 @@
|
||||
scope={metricData.data.scope}
|
||||
timestep={metricData.data.metric.timestep}
|
||||
series={metricData.data.metric.series}
|
||||
statisticsSeries={metricData.data?.metric.statisticsSeries}
|
||||
useStatsSeries={!!metricData.data?.metric.statisticsSeries}
|
||||
statisticsSeries={metricData.data.metric.statisticsSeries}
|
||||
height={175}
|
||||
{plotSync}
|
||||
forNode
|
||||
|
||||
Reference in New Issue
Block a user