From 4083de2a514b91ffd24791e12e63a04622109190 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Tue, 9 Dec 2025 10:26:55 +0100 Subject: [PATCH 1/6] Add public dashboard and route, add DoubleMetricPlot and GQL queries - add roofline legend display switch - small fixes --- api/schema.graphqls | 19 + internal/graph/generated/generated.go | 643 +++++++++++++++++ internal/graph/model/models_gen.go | 12 + internal/graph/schema.resolvers.go | 81 +++ internal/routerConfig/routes.go | 31 +- web/frontend/rollup.config.mjs | 1 + web/frontend/src/DashPublic.root.svelte | 671 ++++++++++++++++++ web/frontend/src/Header.svelte | 2 +- web/frontend/src/Status.root.svelte | 68 +- web/frontend/src/dashpublic.entrypoint.js | 13 + .../src/generic/plots/DoubleMetricPlot.svelte | 640 +++++++++++++++++ .../src/generic/plots/Roofline.svelte | 3 +- web/frontend/src/generic/plots/Stacked.svelte | 2 +- web/frontend/src/header/NavbarLinks.svelte | 28 + web/frontend/src/status.entrypoint.js | 1 + web/frontend/src/status/DashDetails.svelte | 82 +++ web/frontend/src/status/DashInternal.svelte | 605 ++++++++++++++++ .../{ => dashdetails}/StatisticsDash.svelte | 10 +- .../{ => dashdetails}/StatusDash.svelte | 14 +- .../status/{ => dashdetails}/UsageDash.svelte | 8 +- web/templates/base.tmpl | 65 +- web/templates/monitoring/dashboard.tmpl | 14 + web/templates/monitoring/status.tmpl | 1 + 23 files changed, 2918 insertions(+), 96 deletions(-) create mode 100644 web/frontend/src/DashPublic.root.svelte create mode 100644 web/frontend/src/dashpublic.entrypoint.js create mode 100644 web/frontend/src/generic/plots/DoubleMetricPlot.svelte create mode 100644 web/frontend/src/status/DashDetails.svelte create mode 100644 web/frontend/src/status/DashInternal.svelte rename web/frontend/src/status/{ => dashdetails}/StatisticsDash.svelte (92%) rename web/frontend/src/status/{ => dashdetails}/StatusDash.svelte (97%) rename web/frontend/src/status/{ => dashdetails}/UsageDash.svelte (98%) create mode 100644 web/templates/monitoring/dashboard.tmpl diff --git a/api/schema.graphqls b/api/schema.graphqls index 8f5e1c7..1c81e6b 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -164,6 +164,13 @@ type JobMetricWithName { metric: JobMetric! } +type ClusterMetricWithName { + name: String! + unit: Unit + timestep: Int! + data: [NullableFloat!]! +} + type JobMetric { unit: Unit timestep: Int! @@ -267,6 +274,11 @@ type NodeMetrics { metrics: [JobMetricWithName!]! } +type ClusterMetrics { + nodeCount: Int! + metrics: [ClusterMetricWithName!]! +} + type NodesResultList { items: [NodeMetrics!]! offset: Int @@ -385,6 +397,13 @@ type Query { page: PageRequest resolution: Int ): NodesResultList! + + clusterMetrics( + cluster: String! + metrics: [String!] + from: Time! + to: Time! + ): ClusterMetrics! } type Mutation { diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index a3b1a1d..b148942 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -66,6 +66,18 @@ type ComplexityRoot struct { SubClusters func(childComplexity int) int } + ClusterMetricWithName struct { + Data func(childComplexity int) int + Name func(childComplexity int) int + Timestep func(childComplexity int) int + Unit func(childComplexity int) int + } + + ClusterMetrics struct { + Metrics func(childComplexity int) int + NodeCount func(childComplexity int) int + } + ClusterSupport struct { Cluster func(childComplexity int) int SubClusters func(childComplexity int) int @@ -319,6 +331,7 @@ type ComplexityRoot struct { Query struct { AllocatedNodes func(childComplexity int, cluster string) int + ClusterMetrics func(childComplexity int, cluster string, metrics []string, from time.Time, to time.Time) int Clusters func(childComplexity int) int GlobalMetrics func(childComplexity int) int Job func(childComplexity int, id string) int @@ -485,6 +498,7 @@ type QueryResolver interface { RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) + ClusterMetrics(ctx context.Context, cluster string, metrics []string, from time.Time, to time.Time) (*model.ClusterMetrics, error) } type SubClusterResolver interface { NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) @@ -551,6 +565,48 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Cluster.SubClusters(childComplexity), true + case "ClusterMetricWithName.data": + if e.complexity.ClusterMetricWithName.Data == nil { + break + } + + return e.complexity.ClusterMetricWithName.Data(childComplexity), true + + case "ClusterMetricWithName.name": + if e.complexity.ClusterMetricWithName.Name == nil { + break + } + + return e.complexity.ClusterMetricWithName.Name(childComplexity), true + + case "ClusterMetricWithName.timestep": + if e.complexity.ClusterMetricWithName.Timestep == nil { + break + } + + return e.complexity.ClusterMetricWithName.Timestep(childComplexity), true + + case "ClusterMetricWithName.unit": + if e.complexity.ClusterMetricWithName.Unit == nil { + break + } + + return e.complexity.ClusterMetricWithName.Unit(childComplexity), true + + case "ClusterMetrics.metrics": + if e.complexity.ClusterMetrics.Metrics == nil { + break + } + + return e.complexity.ClusterMetrics.Metrics(childComplexity), true + + case "ClusterMetrics.nodeCount": + if e.complexity.ClusterMetrics.NodeCount == nil { + break + } + + return e.complexity.ClusterMetrics.NodeCount(childComplexity), true + case "ClusterSupport.cluster": if e.complexity.ClusterSupport.Cluster == nil { break @@ -1699,6 +1755,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Query.AllocatedNodes(childComplexity, args["cluster"].(string)), true + case "Query.clusterMetrics": + if e.complexity.Query.ClusterMetrics == nil { + break + } + + args, err := ec.field_Query_clusterMetrics_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.ClusterMetrics(childComplexity, args["cluster"].(string), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time)), true + case "Query.clusters": if e.complexity.Query.Clusters == nil { break @@ -2577,6 +2645,13 @@ type JobMetricWithName { metric: JobMetric! } +type ClusterMetricWithName { + name: String! + unit: Unit + timestep: Int! + data: [NullableFloat!]! +} + type JobMetric { unit: Unit timestep: Int! @@ -2680,6 +2755,11 @@ type NodeMetrics { metrics: [JobMetricWithName!]! } +type ClusterMetrics { + nodeCount: Int! + metrics: [ClusterMetricWithName!]! +} + type NodesResultList { items: [NodeMetrics!]! offset: Int @@ -2798,6 +2878,13 @@ type Query { page: PageRequest resolution: Int ): NodesResultList! + + clusterMetrics( + cluster: String! + metrics: [String!] + from: Time! + to: Time! + ): ClusterMetrics! } type Mutation { @@ -3074,6 +3161,32 @@ func (ec *executionContext) field_Query_allocatedNodes_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Query_clusterMetrics_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := graphql.ProcessArgField(ctx, rawArgs, "cluster", ec.unmarshalNString2string) + if err != nil { + return nil, err + } + args["cluster"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "metrics", ec.unmarshalOString2ᚕstringᚄ) + if err != nil { + return nil, err + } + args["metrics"] = arg1 + arg2, err := graphql.ProcessArgField(ctx, rawArgs, "from", ec.unmarshalNTime2timeᚐTime) + if err != nil { + return nil, err + } + args["from"] = arg2 + arg3, err := graphql.ProcessArgField(ctx, rawArgs, "to", ec.unmarshalNTime2timeᚐTime) + if err != nil { + return nil, err + } + args["to"] = arg3 + return args, nil +} + func (ec *executionContext) field_Query_jobMetrics_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -3784,6 +3897,283 @@ func (ec *executionContext) fieldContext_Cluster_subClusters(_ context.Context, return fc, nil } +func (ec *executionContext) _ClusterMetricWithName_name(ctx context.Context, field graphql.CollectedField, obj *model.ClusterMetricWithName) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ClusterMetricWithName_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ClusterMetricWithName_name(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ClusterMetricWithName", + 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) _ClusterMetricWithName_unit(ctx context.Context, field graphql.CollectedField, obj *model.ClusterMetricWithName) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ClusterMetricWithName_unit(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Unit, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*schema.Unit) + fc.Result = res + return ec.marshalOUnit2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋschemaᚐUnit(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ClusterMetricWithName_unit(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ClusterMetricWithName", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "base": + return ec.fieldContext_Unit_base(ctx, field) + case "prefix": + return ec.fieldContext_Unit_prefix(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Unit", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ClusterMetricWithName_timestep(ctx context.Context, field graphql.CollectedField, obj *model.ClusterMetricWithName) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ClusterMetricWithName_timestep(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Timestep, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ClusterMetricWithName_timestep(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ClusterMetricWithName", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ClusterMetricWithName_data(ctx context.Context, field graphql.CollectedField, obj *model.ClusterMetricWithName) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ClusterMetricWithName_data(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Data, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]schema.Float) + fc.Result = res + return ec.marshalNNullableFloat2ᚕgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋschemaᚐFloatᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ClusterMetricWithName_data(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ClusterMetricWithName", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type NullableFloat does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ClusterMetrics_nodeCount(ctx context.Context, field graphql.CollectedField, obj *model.ClusterMetrics) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ClusterMetrics_nodeCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.NodeCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ClusterMetrics_nodeCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ClusterMetrics", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ClusterMetrics_metrics(ctx context.Context, field graphql.CollectedField, obj *model.ClusterMetrics) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ClusterMetrics_metrics(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Metrics, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*model.ClusterMetricWithName) + fc.Result = res + return ec.marshalNClusterMetricWithName2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐClusterMetricWithNameᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ClusterMetrics_metrics(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ClusterMetrics", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "name": + return ec.fieldContext_ClusterMetricWithName_name(ctx, field) + case "unit": + return ec.fieldContext_ClusterMetricWithName_unit(ctx, field) + case "timestep": + return ec.fieldContext_ClusterMetricWithName_timestep(ctx, field) + case "data": + return ec.fieldContext_ClusterMetricWithName_data(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ClusterMetricWithName", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _ClusterSupport_cluster(ctx context.Context, field graphql.CollectedField, obj *schema.ClusterSupport) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ClusterSupport_cluster(ctx, field) if err != nil { @@ -12353,6 +12743,67 @@ func (ec *executionContext) fieldContext_Query_nodeMetricsList(ctx context.Conte return fc, nil } +func (ec *executionContext) _Query_clusterMetrics(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_clusterMetrics(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().ClusterMetrics(rctx, fc.Args["cluster"].(string), fc.Args["metrics"].([]string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.ClusterMetrics) + fc.Result = res + return ec.marshalNClusterMetrics2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐClusterMetrics(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_clusterMetrics(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "nodeCount": + return ec.fieldContext_ClusterMetrics_nodeCount(ctx, field) + case "metrics": + return ec.fieldContext_ClusterMetrics_metrics(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ClusterMetrics", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_clusterMetrics_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -17527,6 +17978,101 @@ func (ec *executionContext) _Cluster(ctx context.Context, sel ast.SelectionSet, return out } +var clusterMetricWithNameImplementors = []string{"ClusterMetricWithName"} + +func (ec *executionContext) _ClusterMetricWithName(ctx context.Context, sel ast.SelectionSet, obj *model.ClusterMetricWithName) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, clusterMetricWithNameImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ClusterMetricWithName") + case "name": + out.Values[i] = ec._ClusterMetricWithName_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "unit": + out.Values[i] = ec._ClusterMetricWithName_unit(ctx, field, obj) + case "timestep": + out.Values[i] = ec._ClusterMetricWithName_timestep(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "data": + out.Values[i] = ec._ClusterMetricWithName_data(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + +var clusterMetricsImplementors = []string{"ClusterMetrics"} + +func (ec *executionContext) _ClusterMetrics(ctx context.Context, sel ast.SelectionSet, obj *model.ClusterMetrics) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, clusterMetricsImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ClusterMetrics") + case "nodeCount": + out.Values[i] = ec._ClusterMetrics_nodeCount(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "metrics": + out.Values[i] = ec._ClusterMetrics_metrics(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var clusterSupportImplementors = []string{"ClusterSupport"} func (ec *executionContext) _ClusterSupport(ctx context.Context, sel ast.SelectionSet, obj *schema.ClusterSupport) graphql.Marshaler { @@ -20101,6 +20647,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "clusterMetrics": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_clusterMetrics(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -21205,6 +21773,74 @@ func (ec *executionContext) marshalNCluster2ᚖgithubᚗcomᚋClusterCockpitᚋc return ec._Cluster(ctx, sel, v) } +func (ec *executionContext) marshalNClusterMetricWithName2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐClusterMetricWithNameᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.ClusterMetricWithName) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNClusterMetricWithName2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐClusterMetricWithName(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNClusterMetricWithName2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐClusterMetricWithName(ctx context.Context, sel ast.SelectionSet, v *model.ClusterMetricWithName) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ClusterMetricWithName(ctx, sel, v) +} + +func (ec *executionContext) marshalNClusterMetrics2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐClusterMetrics(ctx context.Context, sel ast.SelectionSet, v model.ClusterMetrics) graphql.Marshaler { + return ec._ClusterMetrics(ctx, sel, &v) +} + +func (ec *executionContext) marshalNClusterMetrics2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐClusterMetrics(ctx context.Context, sel ast.SelectionSet, v *model.ClusterMetrics) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ClusterMetrics(ctx, sel, v) +} + func (ec *executionContext) marshalNClusterSupport2githubᚗcomᚋClusterCockpitᚋccᚑlibᚋschemaᚐClusterSupport(ctx context.Context, sel ast.SelectionSet, v schema.ClusterSupport) graphql.Marshaler { return ec._ClusterSupport(ctx, sel, &v) } @@ -24142,6 +24778,13 @@ func (ec *executionContext) marshalOUnit2githubᚗcomᚋClusterCockpitᚋccᚑli return ec._Unit(ctx, sel, &v) } +func (ec *executionContext) marshalOUnit2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑlibᚋschemaᚐUnit(ctx context.Context, sel ast.SelectionSet, v *schema.Unit) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Unit(ctx, sel, v) +} + func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐUser(ctx context.Context, sel ast.SelectionSet, v *model.User) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 4cb414e..63b2da5 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -13,6 +13,18 @@ import ( "github.com/ClusterCockpit/cc-lib/schema" ) +type ClusterMetricWithName struct { + Name string `json:"name"` + Unit *schema.Unit `json:"unit,omitempty"` + Timestep int `json:"timestep"` + Data []schema.Float `json:"data"` +} + +type ClusterMetrics struct { + NodeCount int `json:"nodeCount"` + Metrics []*ClusterMetricWithName `json:"metrics"` +} + type Count struct { Name string `json:"name"` Count int `json:"count"` diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index e4901c4..624ddc6 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "math" "regexp" "slices" "strconv" @@ -973,6 +974,86 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub return nodeMetricsListResult, nil } +// ClusterMetrics is the resolver for the clusterMetrics field. +func (r *queryResolver) ClusterMetrics(ctx context.Context, cluster string, metrics []string, from time.Time, to time.Time) (*model.ClusterMetrics, error) { + + user := repository.GetUserFromContext(ctx) + if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { + return nil, errors.New("you need to be administrator or support staff for this query") + } + + if metrics == nil { + for _, mc := range archive.GetCluster(cluster).MetricConfig { + metrics = append(metrics, mc.Name) + } + } + + // 'nodes' == nil -> Defaults to all nodes of cluster for existing query workflow + scopes := []schema.MetricScope{"node"} + data, err := metricDataDispatcher.LoadNodeData(cluster, metrics, nil, scopes, from, to, ctx) + if err != nil { + cclog.Warn("error while loading node data") + return nil, err + } + + clusterMetricData := make([]*model.ClusterMetricWithName, 0) + clusterMetrics := model.ClusterMetrics{NodeCount: 0, Metrics: clusterMetricData} + + collectorTimestep := make(map[string]int) + collectorUnit := make(map[string]schema.Unit) + collectorData := make(map[string][]schema.Float) + + for _, metrics := range data { + clusterMetrics.NodeCount += 1 + for metric, scopedMetrics := range metrics { + _, ok := collectorData[metric] + if !ok { + collectorData[metric] = make([]schema.Float, 0) + for _, scopedMetric := range scopedMetrics { + // Collect Info + collectorTimestep[metric] = scopedMetric.Timestep + collectorUnit[metric] = scopedMetric.Unit + // Collect Initial Data + for _, ser := range scopedMetric.Series { + for _, val := range ser.Data { + collectorData[metric] = append(collectorData[metric], val) + } + } + } + } else { + // Sum up values by index + for _, scopedMetric := range scopedMetrics { + // For This Purpose (Cluster_Wide-Sum of Node Metrics) OK + for _, ser := range scopedMetric.Series { + for i, val := range ser.Data { + collectorData[metric][i] += val + } + } + } + } + } + } + + for metricName, data := range collectorData { + cu := collectorUnit[metricName] + roundedData := make([]schema.Float, 0) + for _, val := range data { + roundedData = append(roundedData, schema.Float((math.Round(float64(val)*100.0) / 100.0))) + } + + cm := model.ClusterMetricWithName{ + Name: metricName, + Unit: &cu, + Timestep: collectorTimestep[metricName], + Data: roundedData, + } + + clusterMetrics.Metrics = append(clusterMetrics.Metrics, &cm) + } + + return &clusterMetrics, nil +} + // NumberOfNodes is the resolver for the numberOfNodes field. func (r *subClusterResolver) NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) { nodeList, err := archive.ParseNodeList(obj.Nodes) diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go index 9c19de5..71edeef 100644 --- a/internal/routerConfig/routes.go +++ b/internal/routerConfig/routes.go @@ -47,7 +47,9 @@ var routes []Route = []Route{ {"/monitoring/systems/list/{cluster}/{subcluster}", "monitoring/systems.tmpl", "Cluster Node List - ClusterCockpit", false, setupClusterListRoute}, {"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node - ClusterCockpit", false, setupNodeRoute}, {"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute}, - {"/monitoring/status/{cluster}", "monitoring/status.tmpl", "Status of - ClusterCockpit", false, setupClusterStatusRoute}, + {"/monitoring/status/{cluster}", "monitoring/status.tmpl", " Dashboard - ClusterCockpit", false, setupClusterStatusRoute}, + {"/monitoring/status/detail/{cluster}", "monitoring/status.tmpl", "Status of - ClusterCockpit", false, setupClusterDetailRoute}, + {"/monitoring/dashboard/{cluster}", "monitoring/dashboard.tmpl", " Dashboard - ClusterCockpit", false, setupDashboardRoute}, } func setupHomeRoute(i InfoType, r *http.Request) InfoType { @@ -117,6 +119,33 @@ func setupClusterStatusRoute(i InfoType, r *http.Request) InfoType { vars := mux.Vars(r) i["id"] = vars["cluster"] i["cluster"] = vars["cluster"] + i["displayType"] = "DASHBOARD" + from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") + if from != "" || to != "" { + i["from"] = from + i["to"] = to + } + return i +} + +func setupClusterDetailRoute(i InfoType, r *http.Request) InfoType { + vars := mux.Vars(r) + i["id"] = vars["cluster"] + i["cluster"] = vars["cluster"] + i["displayType"] = "DETAILS" + from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") + if from != "" || to != "" { + i["from"] = from + i["to"] = to + } + return i +} + +func setupDashboardRoute(i InfoType, r *http.Request) InfoType { + vars := mux.Vars(r) + i["id"] = vars["cluster"] + i["cluster"] = vars["cluster"] + i["displayType"] = "PUBLIC" from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") if from != "" || to != "" { i["from"] = from diff --git a/web/frontend/rollup.config.mjs b/web/frontend/rollup.config.mjs index c92d815..6b7cf88 100644 --- a/web/frontend/rollup.config.mjs +++ b/web/frontend/rollup.config.mjs @@ -74,5 +74,6 @@ export default [ entrypoint('node', 'src/node.entrypoint.js'), entrypoint('analysis', 'src/analysis.entrypoint.js'), entrypoint('status', 'src/status.entrypoint.js'), + entrypoint('dashpublic', 'src/dashpublic.entrypoint.js'), entrypoint('config', 'src/config.entrypoint.js') ]; diff --git a/web/frontend/src/DashPublic.root.svelte b/web/frontend/src/DashPublic.root.svelte new file mode 100644 index 0000000..e344aa4 --- /dev/null +++ b/web/frontend/src/DashPublic.root.svelte @@ -0,0 +1,671 @@ + + + + + + +

{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)} Dashboard

+
+ + {#if $statusQuery.fetching || $statesTimed.fetching || $topJobsQuery.fetching || $nodeStatusQuery.fetching} + + + + + + + {:else if $statusQuery.error || $statesTimed.error || $topJobsQuery.error || $nodeStatusQuery.error} + + {#if $statusQuery.error} + + Error Requesting StatusQuery: {$statusQuery.error.message} + + {/if} + {#if $statesTimed.error} + + Error Requesting StatesTimed: {$statesTimed.error.message} + + {/if} + {#if $topJobsQuery.error} + + Error Requesting TopJobsQuery: {$topJobsQuery.error.message} + + {/if} + {#if $nodeStatusQuery.error} + + Error Requesting NodeStatusQuery: {$nodeStatusQuery.error.message} + + {/if} + + + {:else} + + + + + Cluster "{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}" + {[...clusterInfo?.processorTypes].toString()} + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + {#if clusterInfo?.totalAccs !== 0} + + + + + + {/if} +
{clusterInfo?.runningJobs} Running Jobs{clusterInfo?.activeUsers} Active Users
+ Flop Rate (Any) + + Memory BW Rate +
+ {clusterInfo?.flopRate} + {clusterInfo?.flopRateUnit} + + {clusterInfo?.memBwRate} + {clusterInfo?.memBwRateUnit} +
Allocated Nodes
+ +
{clusterInfo?.allocatedNodes} / {clusterInfo?.totalNodes} + Nodes
Allocated Cores
+ +
{formatNumber(clusterInfo?.allocatedCores)} / {formatNumber(clusterInfo?.totalCores)} + Cores
Allocated Accelerators
+ +
{clusterInfo?.allocatedAccs} / {clusterInfo?.totalAccs} + Accelerators
+
+
+ + + + +
+ {#key refinedStateData} +

+ Current Node States +

+ sd.count, + )} + entities={refinedStateData.map( + (sd) => sd.state, + )} + /> + {/key} +
+ + + {#key refinedStateData} + + + + + + + {#each refinedStateData as sd, i} + + + + + + {/each} +
Current StateNodes
{sd.state}{sd.count}
+ {/key} + +
+ + + + + + Infos + + + Contents + + + + +
+ {#key $statusQuery?.data?.nodeMetrics} + + {/key} +
+ + +
+ +
+ + + +
+ {#key $statesTimed?.data?.nodeStates} + + {/key} +
+ +
+ {/if} +
+
diff --git a/web/frontend/src/Header.svelte b/web/frontend/src/Header.svelte index 98a796a..f7ceac2 100644 --- a/web/frontend/src/Header.svelte +++ b/web/frontend/src/Header.svelte @@ -120,7 +120,7 @@ href: "/monitoring/status/", icon: "clipboard-data", perCluster: true, - listOptions: false, + listOptions: true, menu: "Info", }, ]; diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte index 3d9002a..8175681 100644 --- a/web/frontend/src/Status.root.svelte +++ b/web/frontend/src/Status.root.svelte @@ -6,77 +6,43 @@ --> - - - + - -{#if $initq.fetching} - +{#if displayType !== "DASHBOARD" && displayType !== "DETAILS"} + - - - -{:else if $initq.error} - - - {$initq.error.message} + Unknown displayList type! {:else} - - - - - - - - - - - - - - - - - - - - - + {#if displayStatusDetail} + + + {:else} + + + {/if} {/if} diff --git a/web/frontend/src/dashpublic.entrypoint.js b/web/frontend/src/dashpublic.entrypoint.js new file mode 100644 index 0000000..47287c7 --- /dev/null +++ b/web/frontend/src/dashpublic.entrypoint.js @@ -0,0 +1,13 @@ +import { mount } from 'svelte'; +// import {} from './header.entrypoint.js' +import DashPublic from './DashPublic.root.svelte' + +mount(DashPublic, { + target: document.getElementById('svelte-app'), + props: { + presetCluster: infos.cluster, + }, + context: new Map([ + ['cc-config', clusterCockpitConfig] + ]) +}) diff --git a/web/frontend/src/generic/plots/DoubleMetricPlot.svelte b/web/frontend/src/generic/plots/DoubleMetricPlot.svelte new file mode 100644 index 0000000..94acf45 --- /dev/null +++ b/web/frontend/src/generic/plots/DoubleMetricPlot.svelte @@ -0,0 +1,640 @@ + + + + + +{#if metricData[0]?.data && metricData[0]?.data?.length > 0} +
+{:else} + Cannot render plot: No series data returned for {cluster} +{/if} diff --git a/web/frontend/src/generic/plots/Roofline.svelte b/web/frontend/src/generic/plots/Roofline.svelte index 79ece22..6425275 100644 --- a/web/frontend/src/generic/plots/Roofline.svelte +++ b/web/frontend/src/generic/plots/Roofline.svelte @@ -36,6 +36,7 @@ subCluster = null, allowSizeChange = false, useColors = true, + useLegend = true, width = 600, height = 380, } = $props(); @@ -534,7 +535,7 @@ width: width, height: height, legend: { - show: true, + show: useLegend, }, cursor: { dataIdx: (u, seriesIdx) => { diff --git a/web/frontend/src/generic/plots/Stacked.svelte b/web/frontend/src/generic/plots/Stacked.svelte index 4c532db..2616f9a 100644 --- a/web/frontend/src/generic/plots/Stacked.svelte +++ b/web/frontend/src/generic/plots/Stacked.svelte @@ -156,7 +156,7 @@ { scale: "y", grid: { show: true }, - labelFont: "sans-serif", + // labelFont: "sans-serif", label: ylabel + (yunit ? ` (${yunit})` : ''), // values: (u, vals) => vals.map((v) => formatNumber(v)), }, diff --git a/web/frontend/src/header/NavbarLinks.svelte b/web/frontend/src/header/NavbarLinks.svelte index 41454d1..04fec0e 100644 --- a/web/frontend/src/header/NavbarLinks.svelte +++ b/web/frontend/src/header/NavbarLinks.svelte @@ -64,6 +64,34 @@ {/each} + {:else if item.title === 'Status'} + + + + {item.title} + + + {#each clusters as cluster} + + + {cluster.name} + + + + Status Dashboard + + + Status Details + + + + {/each} + + {:else} diff --git a/web/frontend/src/status.entrypoint.js b/web/frontend/src/status.entrypoint.js index c3407c1..e21b361 100644 --- a/web/frontend/src/status.entrypoint.js +++ b/web/frontend/src/status.entrypoint.js @@ -6,6 +6,7 @@ mount(Status, { target: document.getElementById('svelte-app'), props: { presetCluster: infos.cluster, + displayType: displayType, }, context: new Map([ ['cc-config', clusterCockpitConfig] diff --git a/web/frontend/src/status/DashDetails.svelte b/web/frontend/src/status/DashDetails.svelte new file mode 100644 index 0000000..410d8df --- /dev/null +++ b/web/frontend/src/status/DashDetails.svelte @@ -0,0 +1,82 @@ + + + + + + + + +

Current Status of Cluster "{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}"

+ +
+ + +{#if $initq.fetching} + + + + + +{:else if $initq.error} + + + {$initq.error.message} + + +{:else} + + + + + + + + + + + + + + + + + + + + + +{/if} diff --git a/web/frontend/src/status/DashInternal.svelte b/web/frontend/src/status/DashInternal.svelte new file mode 100644 index 0000000..73fa639 --- /dev/null +++ b/web/frontend/src/status/DashInternal.svelte @@ -0,0 +1,605 @@ + + + + + + +

{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)} Dashboard

+
+ + {#if $statusQuery.fetching || $statesTimed.fetching || $topJobsQuery.fetching || $nodeStatusQuery.fetching} + + + + + + + {:else if $statusQuery.error || $statesTimed.error || $topJobsQuery.error || $nodeStatusQuery.error} + + {#if $statusQuery.error} + + Error Requesting StatusQuery: {$statusQuery.error.message} + + {/if} + {#if $statesTimed.error} + + Error Requesting StatesTimed: {$statesTimed.error.message} + + {/if} + {#if $topJobsQuery.error} + + Error Requesting TopJobsQuery: {$topJobsQuery.error.message} + + {/if} + {#if $nodeStatusQuery.error} + + Error Requesting NodeStatusQuery: {$nodeStatusQuery.error.message} + + {/if} + + + {:else} + + + + + Cluster "{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}" + {[...clusterInfo?.processorTypes].toString()} + + + + + + + +
+ + + + + + + + +
+ + + + + + + + + + + {#if clusterInfo?.totalAccs !== 0} + + + + + + {/if} +
{clusterInfo?.runningJobs} Running Jobs{clusterInfo?.activeUsers} Active Users
+ Flop Rate (Any) + + Memory BW Rate +
+ {clusterInfo?.flopRate} + {clusterInfo?.flopRateUnit} + + {clusterInfo?.memBwRate} + {clusterInfo?.memBwRateUnit} +
Allocated Nodes
+ +
{clusterInfo?.allocatedNodes} / {clusterInfo?.totalNodes} + Nodes
Allocated Cores
+ +
{formatNumber(clusterInfo?.allocatedCores)} / {formatNumber(clusterInfo?.totalCores)} + Cores
Allocated Accelerators
+ +
{clusterInfo?.allocatedAccs} / {clusterInfo?.totalAccs} + Accelerators
+
+
+ + + + +
+

+ Top Projects: Jobs +

+ tp['totalJobs'], + )} + entities={$topJobsQuery.data.jobsStatistics.map((tp) => scrambleNames ? scramble(tp.id) : tp.id)} + /> +
+ + + + + + + + + {#each $topJobsQuery.data.jobsStatistics as tp, i} + + + + + + {/each} +
ProjectJobs
+ {scrambleNames ? scramble(tp.id) : tp.id} + + {tp['totalJobs']}
+ +
+ + +
+ {#key $statusQuery?.data?.jobsMetricStats} + + {/key} +
+ + + {#if clusterInfo?.totalAccs == 0} + + {:else} + + {/if} + + +
+ {#key $statesTimed?.data?.nodeStates} + + {/key} +
+ + +
+ {#key $statesTimed?.data?.healthStates} + + {/key} +
+ +
+ {/if} +
+
diff --git a/web/frontend/src/status/StatisticsDash.svelte b/web/frontend/src/status/dashdetails/StatisticsDash.svelte similarity index 92% rename from web/frontend/src/status/StatisticsDash.svelte rename to web/frontend/src/status/dashdetails/StatisticsDash.svelte index efd7a4c..0a1cd46 100644 --- a/web/frontend/src/status/StatisticsDash.svelte +++ b/web/frontend/src/status/dashdetails/StatisticsDash.svelte @@ -22,11 +22,11 @@ } from "@urql/svelte"; import { convert2uplot, - } from "../generic/utils.js"; - import PlotGrid from "../generic/PlotGrid.svelte"; - import Histogram from "../generic/plots/Histogram.svelte"; - import HistogramSelection from "../generic/select/HistogramSelection.svelte"; - import Refresher from "../generic/helper/Refresher.svelte"; + } from "../../generic/utils.js"; + import PlotGrid from "../../generic/PlotGrid.svelte"; + import Histogram from "../../generic/plots/Histogram.svelte"; + import HistogramSelection from "../../generic/select/HistogramSelection.svelte"; + import Refresher from "../../generic/helper/Refresher.svelte"; /* Svelte 5 Props */ let { diff --git a/web/frontend/src/status/StatusDash.svelte b/web/frontend/src/status/dashdetails/StatusDash.svelte similarity index 97% rename from web/frontend/src/status/StatusDash.svelte rename to web/frontend/src/status/dashdetails/StatusDash.svelte index 03a8cc4..ee60483 100644 --- a/web/frontend/src/status/StatusDash.svelte +++ b/web/frontend/src/status/dashdetails/StatusDash.svelte @@ -22,12 +22,12 @@ gql, getContextClient, } from "@urql/svelte"; - import { formatDurationTime } from "../generic/units.js"; - import Refresher from "../generic/helper/Refresher.svelte"; - import TimeSelection from "../generic/select/TimeSelection.svelte"; - import Roofline from "../generic/plots/Roofline.svelte"; - import Pie, { colors } from "../generic/plots/Pie.svelte"; - import Stacked from "../generic/plots/Stacked.svelte"; + import { formatDurationTime } from "../../generic/units.js"; + import Refresher from "../../generic/helper/Refresher.svelte"; + import TimeSelection from "../../generic/select/TimeSelection.svelte"; + import Roofline from "../../generic/plots/Roofline.svelte"; + import Pie, { colors } from "../../generic/plots/Pie.svelte"; + import Stacked from "../../generic/plots/Stacked.svelte"; /* Svelte 5 Props */ let { @@ -83,7 +83,7 @@ } `, variables: { - filter: { cluster: { eq: cluster }, timeStart: stackedFrom}, + filter: { cluster: { eq: cluster }, timeStart: 1760096999}, typeNode: "node", typeHealth: "health" }, diff --git a/web/frontend/src/status/UsageDash.svelte b/web/frontend/src/status/dashdetails/UsageDash.svelte similarity index 98% rename from web/frontend/src/status/UsageDash.svelte rename to web/frontend/src/status/dashdetails/UsageDash.svelte index 74dd7a9..2071465 100644 --- a/web/frontend/src/status/UsageDash.svelte +++ b/web/frontend/src/status/dashdetails/UsageDash.svelte @@ -27,10 +27,10 @@ scramble, scrambleNames, convert2uplot, - } from "../generic/utils.js"; - import Pie, { colors } from "../generic/plots/Pie.svelte"; - import Histogram from "../generic/plots/Histogram.svelte"; - import Refresher from "../generic/helper/Refresher.svelte"; + } from "../../generic/utils.js"; + import Pie, { colors } from "../../generic/plots/Pie.svelte"; + import Histogram from "../../generic/plots/Histogram.svelte"; + import Refresher from "../../generic/helper/Refresher.svelte"; /* Svelte 5 Props */ let { diff --git a/web/templates/base.tmpl b/web/templates/base.tmpl index 7fd35a0..28eab33 100644 --- a/web/templates/base.tmpl +++ b/web/templates/base.tmpl @@ -23,34 +23,49 @@ - {{block "navigation" .}} -
- {{end}} + {{if eq .Infos.displayType "PUBLIC"}} +
+
+ {{block "content-public" .}} + Whoops, you should not see this... [MAIN] + {{end}} +
+
-
-
- {{block "content" .}} - Whoops, you should not see this... - {{end}} -
-
+ {{block "javascript-public" .}} + Whoops, you should not see this... [JS] + {{end}} - {{block "footer" .}} -
- -
    -
  • Version {{ .Build.Version }}
  • -
  • Hash {{ .Build.Hash }}
  • -
  • Built {{ .Build.Buildtime }}
  • -
-
- {{end}} + {{else}} + {{block "navigation" .}} +
+ {{end}} - {{block "javascript" .}} - +
+
+ {{block "content" .}} + Whoops, you should not see this... [MAIN] + {{end}} +
+
+ + {{block "footer" .}} +
+ +
    +
  • Version {{ .Build.Version }}
  • +
  • Hash {{ .Build.Hash }}
  • +
  • Built {{ .Build.Buildtime }}
  • +
+
+ {{end}} + + {{block "javascript" .}} + + {{end}} {{end}} diff --git a/web/templates/monitoring/dashboard.tmpl b/web/templates/monitoring/dashboard.tmpl new file mode 100644 index 0000000..06666cd --- /dev/null +++ b/web/templates/monitoring/dashboard.tmpl @@ -0,0 +1,14 @@ +{{define "content-public"}} +
+{{end}} + +{{define "stylesheets"}} + +{{end}} +{{define "javascript-public"}} + + +{{end}} diff --git a/web/templates/monitoring/status.tmpl b/web/templates/monitoring/status.tmpl index 15aff69..b6a8414 100644 --- a/web/templates/monitoring/status.tmpl +++ b/web/templates/monitoring/status.tmpl @@ -8,6 +8,7 @@ {{define "javascript"}} From 6e385db378b314f0ba31e0b74d05a4baa30b7da1 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Thu, 11 Dec 2025 18:51:19 +0100 Subject: [PATCH 2/6] color roofline plot, add option to match pie and table color for ndoestate --- web/frontend/src/DashPublic.root.svelte | 201 +++++++++--------- .../src/generic/plots/DoubleMetricPlot.svelte | 4 +- web/frontend/src/generic/plots/Pie.svelte | 13 +- .../src/generic/plots/Roofline.svelte | 189 ++++++++++++++-- .../src/generic/plots/RooflineLegacy.svelte | 8 +- web/frontend/src/generic/plots/Stacked.svelte | 18 +- 6 files changed, 304 insertions(+), 129 deletions(-) diff --git a/web/frontend/src/DashPublic.root.svelte b/web/frontend/src/DashPublic.root.svelte index e344aa4..2e06012 100644 --- a/web/frontend/src/DashPublic.root.svelte +++ b/web/frontend/src/DashPublic.root.svelte @@ -82,7 +82,7 @@ } `, variables: { - filter: { cluster: { eq: presetCluster }, timeStart: 1760096999}, // DEBUG VALUE, use StackedFrom + filter: { cluster: { eq: presetCluster }, timeStart: stackedFrom}, // DEBUG VALUE 1760096999, use StackedFrom typeNode: "node", typeHealth: "health" }, @@ -97,6 +97,7 @@ query ( $cluster: String! $metrics: [String!] + # $nmetrics: [String!] $from: Time! $to: Time! $clusterFrom: Time! @@ -183,7 +184,7 @@ totalCores totalAccs } - # TEST + # ClusterMetrics for doubleMetricPlot clusterMetrics( cluster: $cluster metrics: $metrics @@ -205,7 +206,8 @@ `, variables: { cluster: presetCluster, - metrics: ["flops_any", "mem_bw"], // Fixed names for roofline and status bars + metrics: ["flops_any", "mem_bw"], // Metrics For Cluster Plot and Roofline + // nmetrics: ["cpu_load", "acc_utilization"], // Metrics for Node Graph from: from.toISOString(), clusterFrom: clusterFrom.toISOString(), to: to.toISOString(), @@ -349,18 +351,18 @@ }); /* Functions */ - function legendColors(targetIdx, useAltColors) { - // Reuses first color if targetIdx overflows - let c; - if (useCbColors) { - c = [...colors['colorblind']]; - } else if (useAltColors) { - c = [...colors['alternative']]; - } else { - c = [...colors['default']]; - } - return c[(c.length + targetIdx) % c.length]; - } + // function legendColors(targetIdx, useAltColors) { + // // Reuses first color if targetIdx overflows + // let c; + // if (useCbColors) { + // c = [...colors['colorblind']]; + // } else if (useAltColors) { + // c = [...colors['alternative']]; + // } else { + // c = [...colors['default']]; + // } + // return c[(c.length + targetIdx) % c.length]; + // } function transformNodesStatsToData(subclusterData) { let data = null @@ -425,10 +427,10 @@ - + + {#if $statusQuery.fetching || $statesTimed.fetching || $topJobsQuery.fetching || $nodeStatusQuery.fetching} @@ -461,13 +463,24 @@ {:else} - - - - + + + + +

Cluster {presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}

+
+ +

CPU(s)

{[...clusterInfo?.processorTypes].join(', ')}

+
+
+ + + + + @@ -545,77 +558,7 @@ - - - -
- {#key refinedStateData} -

- Current Node States -

- sd.count, - )} - entities={refinedStateData.map( - (sd) => sd.state, - )} - /> - {/key} -
- - - {#key refinedStateData} -
- - - - - - {#each refinedStateData as sd, i} - - - - - - {/each} -
Current StateNodes
{sd.state}{sd.count}
- {/key} - -
- - - - - - Infos - - - Contents - - - - -
- {#key $statusQuery?.data?.nodeMetrics} - - {/key} -
- +
+ + +
+ {#key $statusQuery?.data?.nodeMetrics} + + {/key} +
+ + + + +
+ {#key refinedStateData} + + sd.count, + )} + entities={refinedStateData.map( + (sd) => sd.state, + )} + fixColors={refinedStateData.map( + (sd) => colors['nodeStates'][sd.state], + )} + /> + {/key} +
+ + + {#key refinedStateData} + + + + + + + {#each refinedStateData as sd, i} + + + + + + {/each} +
StateCount
{sd.state.charAt(0).toUpperCase() + sd.state.slice(1)}{sd.count}
+ {/key} + +
+ +
{#key $statesTimed?.data?.nodeStates} @@ -659,7 +670,7 @@ xlabel="Time" ylabel="Nodes" yunit = "#Count" - title = "Node States" + title = "Cluster Status" stateType = "Node" /> {/key} diff --git a/web/frontend/src/generic/plots/DoubleMetricPlot.svelte b/web/frontend/src/generic/plots/DoubleMetricPlot.svelte index 94acf45..9579f36 100644 --- a/web/frontend/src/generic/plots/DoubleMetricPlot.svelte +++ b/web/frontend/src/generic/plots/DoubleMetricPlot.svelte @@ -33,6 +33,7 @@ // metric, width = 0, height = 300, + fixLinewidth = null, timestep, numNodes, metricData, @@ -52,7 +53,7 @@ // const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster); // const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric); const lineColors = clusterCockpitConfig.plotConfiguration_colorScheme; - const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio; + const lineWidth = fixLinewidth ? fixLinewidth : clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio; // const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false; const renderSleepTime = 200; // const normalLineColor = "#000000"; @@ -444,6 +445,7 @@ const opts = { width, height, + title: 'Cluster Utilization', plugins: [legendAsTooltipPlugin()], series: plotSeries, axes: [ diff --git a/web/frontend/src/generic/plots/Pie.svelte b/web/frontend/src/generic/plots/Pie.svelte index 45be1f5..5e31b7d 100644 --- a/web/frontend/src/generic/plots/Pie.svelte +++ b/web/frontend/src/generic/plots/Pie.svelte @@ -59,7 +59,15 @@ 'rgb(135,133,0)', 'rgb(0,167,108)', 'rgb(189,189,189)', - ] + ], + nodeStates: { + allocated: "rgba(0, 128, 0, 0.75)", + down: "rgba(255, 0, 0, 0.75)", + idle: "rgba(0, 0, 255, 0.75)", + reserved: "rgba(255, 0, 255, 0.75)", + mixed: "rgba(255, 215, 0, 0.75)", + unknown: "rgba(0, 0, 0, 0.75)" + } } @@ -77,6 +85,7 @@ entities, displayLegend = false, useAltColors = false, + fixColors = null } = $props(); /* Const Init */ @@ -98,6 +107,8 @@ c = [...colors['colorblind']]; } else if (useAltColors) { c = [...colors['alternative']]; + } else if (fixColors?.length > 0) { + c = [...fixColors]; } else { c = [...colors['default']]; } diff --git a/web/frontend/src/generic/plots/Roofline.svelte b/web/frontend/src/generic/plots/Roofline.svelte index 6425275..c28ab1f 100644 --- a/web/frontend/src/generic/plots/Roofline.svelte +++ b/web/frontend/src/generic/plots/Roofline.svelte @@ -34,15 +34,18 @@ nodesData = null, cluster = null, subCluster = null, + fixTitle = null, + yMinimum = null, allowSizeChange = false, useColors = true, useLegend = true, + colorBackground = false, width = 600, height = 380, } = $props(); /* Const Init */ - const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth; + const lineWidth = 2 // clusterCockpitConfig.plotConfiguration_lineWidth; const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false; /* Var Init */ @@ -294,7 +297,7 @@ } else { // No Colors: Use Black u.ctx.strokeStyle = "rgb(0, 0, 0)"; - u.ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; + u.ctx.fillStyle = colorBackground ? "rgb(0, 0, 0)" : "rgba(0, 0, 0, 0.5)"; } // Get Values @@ -527,6 +530,7 @@ let plotTitle = "CPU Roofline Diagram"; if (jobsData) plotTitle = "Job Average Roofline Diagram"; if (nodesData) plotTitle = "Node Average Roofline Diagram"; + if (fixTitle) plotTitle = fixTitle if (roofData) { const opts = { @@ -617,7 +621,7 @@ }, y: { range: [ - 0.01, + yMinimum ? yMinimum : 0.01, subCluster?.flopRateSimd?.value ? nearestThousand(subCluster.flopRateSimd.value) : 10000, @@ -669,6 +673,7 @@ u.ctx.lineWidth = lineWidth; u.ctx.beginPath(); + // Get Values const ycut = 0.01 * subCluster.memoryBandwidth.value; const scalarKnee = (subCluster.flopRateScalar.value - ycut) / @@ -676,19 +681,27 @@ const simdKnee = (subCluster.flopRateSimd.value - ycut) / subCluster.memoryBandwidth.value; - const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels - simdKneeX = u.valToPos(simdKnee, "x", true), - flopRateScalarY = u.valToPos( - subCluster.flopRateScalar.value, - "y", - true, - ), - flopRateSimdY = u.valToPos( - subCluster.flopRateSimd.value, - "y", - true, - ); + // Get Const Coords + const originX = u.valToPos(0.01, "x", true); + const originY = u.valToPos(yMinimum ? yMinimum : 0.01, "y", true); + + const outerX = u.valToPos(1000, "x", true); // rightmost x in plot coords + const outerY = u.valToPos( + subCluster?.flopRateSimd?.value + ? nearestThousand(subCluster.flopRateSimd.value) + : 10000, + "y", + true + ); + + const scalarKneeX = u.valToPos(scalarKnee, "x", true) // Value, axis, toCanvasPixels + const simdKneeX = u.valToPos(simdKnee, "x", true) + + const flopRateScalarY = u.valToPos(subCluster.flopRateScalar.value, "y", true) + const flopRateSimdY = u.valToPos(subCluster.flopRateSimd.value, "y", true); + + /* Render Lines */ if ( scalarKneeX < width * window.devicePixelRatio - @@ -728,10 +741,10 @@ y1, x2, y2, - u.valToPos(0.01, "x", true), - u.valToPos(1.0, "y", true), // X-Axis Start Coords - u.valToPos(1000, "x", true), - u.valToPos(1.0, "y", true), // X-Axis End Coords + originX, // x3; X-Axis Start Coord-X + originY, // y3; X-Axis Start Coord-Y + outerX, // x4; X-Axis End Coord-X + originY, // y4; X-Axis End Coord-Y ); if (xAxisIntersect.x > x1) { @@ -746,6 +759,144 @@ u.ctx.stroke(); // Reset grid lineWidth u.ctx.lineWidth = 0.15; + + /* Render Area */ + if (colorBackground) { + + u.ctx.beginPath(); + // Additional Coords for Colored Regions + const yhalf = u.valToPos(ycut/2, "y", true) + const simdShift = u.valToPos(simdKnee*1.75, "x", true) + + let upperBorderIntersect = lineIntersect( + x1, + y1, + x2, + y2, + originX, // x3; X-Axis Start Coord-X + flopRateSimdY*1.667, // y3; X-Axis Start Coord-Y + outerX, // x4; X-Axis End Coord-X + flopRateSimdY*1.667, // y4; X-Axis End Coord-Y + ); + + let lowerBorderIntersect = lineIntersect( + x1, + y1, + x2, + y2, + originX, // x3; X-Axis Start Coord-X + flopRateScalarY*1.1667, // y3; X-Axis Start Coord-Y + outerX, // x4; X-Axis End Coord-X + flopRateScalarY*1.1667, // y4; X-Axis End Coord-Y + ); + + let helperUpperBorderIntersect = lineIntersect( + x1, + yhalf, + simdShift, + y2, + originX, // x3; X-Axis Start Coord-X + flopRateSimdY*1.667, // y3; X-Axis Start Coord-Y + outerX, // x4; X-Axis End Coord-X + flopRateSimdY*1.667, // y4; X-Axis End Coord-Y + ); + + let helperLowerBorderIntersect = lineIntersect( + x1, + yhalf, + simdShift, + y2, + originX, // x3; X-Axis Start Coord-X + flopRateScalarY*1.1667, // y3; X-Axis Start Coord-Y + outerX, // x4; X-Axis End Coord-X + flopRateScalarY*1.1667, // y4; X-Axis End Coord-Y + ); + + let helperLowerBorderIntersectTop = lineIntersect( + x1, + yhalf, + simdShift, + y2, + scalarKneeX, // x3; X-Axis Start Coord-X + flopRateScalarY, // y3; X-Axis Start Coord-Y + outerX, // x4; X-Axis End Coord-X + flopRateScalarY, // y4; X-Axis End Coord-Y + ); + + // Diagonal Helper + u.ctx.moveTo(x1, yhalf); + u.ctx.lineTo(simdShift, y2); + // Upper Simd Helper + u.ctx.moveTo(upperBorderIntersect.x, flopRateSimdY*1.667); + u.ctx.lineTo(outerX, flopRateSimdY*1.667); + // Lower Scalar Helper + u.ctx.moveTo(lowerBorderIntersect.x, flopRateScalarY*1.1667); + u.ctx.lineTo(outerX, flopRateScalarY*1.1667); + + u.ctx.stroke(); + + /* Color Regions */ + // MemoryBound + u.ctx.save(); + u.ctx.beginPath(); + u.ctx.lineTo(x1, y1); // YCut + u.ctx.lineTo(x2, y2); // Upper Knee + u.ctx.lineTo(simdShift, y2); // Upper Helper Knee + u.ctx.lineTo(x1, yhalf); // Half yCut + u.ctx.closePath(); + u.ctx.fillStyle = "rgba(255, 200, 0, 0.4)"; // Yellow + u.ctx.fill(); + u.ctx.restore(); + + // Compute Lower + u.ctx.save(); + u.ctx.beginPath(); + u.ctx.moveTo(lowerBorderIntersect.x, flopRateScalarY*1.1667); // Lower Helper Knee + u.ctx.lineTo(scalarKneeX, flopRateScalarY); // Lower Knee + u.ctx.lineTo(outerX, flopRateScalarY); // Outer Border + u.ctx.lineTo(outerX, flopRateScalarY*1.1667); // Outer Lower Helper Border + u.ctx.closePath(); + u.ctx.fillStyle = "rgba(0, 180, 255, 0.4)"; // Cyan Blue + u.ctx.fill(); + u.ctx.restore(); + + // Compute Upper + u.ctx.save(); + u.ctx.beginPath(); + u.ctx.moveTo(upperBorderIntersect.x, flopRateSimdY*1.667); // Upper Helper Knee + u.ctx.lineTo(simdKneeX, flopRateSimdY); // Upper Knee + u.ctx.lineTo(outerX, flopRateSimdY); // Outer Border + u.ctx.lineTo(outerX, flopRateSimdY*1.667); // Outer Upper Helper Border + u.ctx.closePath(); + u.ctx.fillStyle = "rgba(0, 180, 255, 0.4)"; // Cyan Blue + u.ctx.fill(); + u.ctx.restore(); + + // Nomansland Lower + u.ctx.save(); + u.ctx.beginPath(); + u.ctx.moveTo(originX, originY); // Origin + u.ctx.lineTo(originX, yhalf); // YCut Half + u.ctx.lineTo(helperLowerBorderIntersect.x, flopRateScalarY*1.1667); // Lower Inner Helper Knee + u.ctx.lineTo(outerX, flopRateScalarY*1.1667); // Lower Inner Border + u.ctx.lineTo(outerX, originY); // Lower Right Corner + u.ctx.closePath(); + u.ctx.fillStyle = "rgba(255, 50, 50, 0.1)"; // Red Light + u.ctx.fill(); + u.ctx.restore(); + + // Nomansland Upper + u.ctx.save(); + u.ctx.beginPath(); + u.ctx.moveTo(helperLowerBorderIntersectTop.x, flopRateScalarY); // Lower Knee Top + u.ctx.lineTo(helperUpperBorderIntersect.x, flopRateSimdY*1.667); // Upper Helper Knee + u.ctx.lineTo(outerX, flopRateSimdY*1.667); // Upper Inner Border + u.ctx.lineTo(outerX, flopRateScalarY); // Lower Knee Border + u.ctx.closePath(); + u.ctx.fillStyle = "rgba(255, 50, 50, 0.1)"; // Red Light + u.ctx.fill(); + u.ctx.restore(); + } } /* Render Scales */ diff --git a/web/frontend/src/generic/plots/RooflineLegacy.svelte b/web/frontend/src/generic/plots/RooflineLegacy.svelte index 624253b..6ee96ef 100644 --- a/web/frontend/src/generic/plots/RooflineLegacy.svelte +++ b/web/frontend/src/generic/plots/RooflineLegacy.svelte @@ -315,10 +315,10 @@ y1, x2, y2, - u.valToPos(0.01, "x", true), - u.valToPos(1.0, "y", true), // X-Axis Start Coords - u.valToPos(1000, "x", true), - u.valToPos(1.0, "y", true), // X-Axis End Coords + u.valToPos(0.01, "x", true), // x3; X-Axis Start Coord-X + u.valToPos(1.0, "y", true), // y3; X-Axis Start Coord-Y + u.valToPos(1000, "x", true), // x4; X-Axis End Coord-X + u.valToPos(1.0, "y", true), // y4; X-Axis End Coord-Y ); if (xAxisIntersect.x > x1) { diff --git a/web/frontend/src/generic/plots/Stacked.svelte b/web/frontend/src/generic/plots/Stacked.svelte index 2616f9a..c9ec1d7 100644 --- a/web/frontend/src/generic/plots/Stacked.svelte +++ b/web/frontend/src/generic/plots/Stacked.svelte @@ -39,63 +39,63 @@ label: "Full", scale: "y", width: lineWidth, - fill: cbmode ? "rgba(0, 110, 0, 0.4)" : "rgba(0, 128, 0, 0.4)", + fill: cbmode ? "rgba(0, 110, 0, 0.6)" : "rgba(0, 128, 0, 0.6)", stroke: cbmode ? "rgb(0, 110, 0)" : "green", }, partial: { label: "Partial", scale: "y", width: lineWidth, - fill: cbmode ? "rgba(235, 172, 35, 0.4)" : "rgba(255, 215, 0, 0.4)", + fill: cbmode ? "rgba(235, 172, 35, 0.6)" : "rgba(255, 215, 0, 0.6)", stroke: cbmode ? "rgb(235, 172, 35)" : "gold", }, failed: { label: "Failed", scale: "y", width: lineWidth, - fill: cbmode ? "rgb(181, 29, 20, 0.4)" : "rgba(255, 0, 0, 0.4)", + fill: cbmode ? "rgb(181, 29, 20, 0.6)" : "rgba(255, 0, 0, 0.6)", stroke: cbmode ? "rgb(181, 29, 20)" : "red", }, idle: { label: "Idle", scale: "y", width: lineWidth, - fill: cbmode ? "rgba(0, 140, 249, 0.4)" : "rgba(0, 0, 255, 0.4)", + fill: cbmode ? "rgba(0, 140, 249, 0.6)" : "rgba(0, 0, 255, 0.6)", stroke: cbmode ? "rgb(0, 140, 249)" : "blue", }, allocated: { label: "Allocated", scale: "y", width: lineWidth, - fill: cbmode ? "rgba(0, 110, 0, 0.4)" : "rgba(0, 128, 0, 0.4)", + fill: cbmode ? "rgba(0, 110, 0, 0.6)" : "rgba(0, 128, 0, 0.6)", stroke: cbmode ? "rgb(0, 110, 0)" : "green", }, reserved: { label: "Reserved", scale: "y", width: lineWidth, - fill: cbmode ? "rgba(209, 99, 230, 0.4)" : "rgba(255, 0, 255, 0.4)", + fill: cbmode ? "rgba(209, 99, 230, 0.6)" : "rgba(255, 0, 255, 0.6)", stroke: cbmode ? "rgb(209, 99, 230)" : "magenta", }, mixed: { label: "Mixed", scale: "y", width: lineWidth, - fill: cbmode ? "rgba(235, 172, 35, 0.4)" : "rgba(255, 215, 0, 0.4)", + fill: cbmode ? "rgba(235, 172, 35, 0.6)" : "rgba(255, 215, 0, 0.6)", stroke: cbmode ? "rgb(235, 172, 35)" : "gold", }, down: { label: "Down", scale: "y", width: lineWidth, - fill: cbmode ? "rgba(181, 29 ,20, 0.4)" : "rgba(255, 0, 0, 0.4)", + fill: cbmode ? "rgba(181, 29 ,20, 0.6)" : "rgba(255, 0, 0, 0.6)", stroke: cbmode ? "rgb(181, 29, 20)" : "red", }, unknown: { label: "Unknown", scale: "y", width: lineWidth, - fill: "rgba(0, 0, 0, 0.4)", + fill: "rgba(0, 0, 0, 0.6)", stroke: "black", } }; From 0d62181272e3d406b2543aab2d8fd765d2245594 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 12 Dec 2025 11:19:37 +0100 Subject: [PATCH 3/6] move roofline elements below series data render --- web/frontend/src/generic/plots/Roofline.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/frontend/src/generic/plots/Roofline.svelte b/web/frontend/src/generic/plots/Roofline.svelte index c28ab1f..3b53a66 100644 --- a/web/frontend/src/generic/plots/Roofline.svelte +++ b/web/frontend/src/generic/plots/Roofline.svelte @@ -651,7 +651,7 @@ hooks: { // setSeries: [ (u, seriesIdx) => console.log('setSeries', seriesIdx) ], // setLegend: [ u => console.log('setLegend', u.legend.idxs) ], - drawClear: [ + drawClear: [ // drawClear hook which fires before anything exists, so will render under the grid (u) => { qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height); qt.clear(); @@ -663,7 +663,7 @@ }); }, ], - draw: [ + drawAxes: [ // drawAxes hook, which fires after axes and grid have been rendered (u) => { // draw roofs when subCluster set if (subCluster != null) { From 79e1c236fe576b98ae8495ea897aa9a6af63f889 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 12 Dec 2025 17:51:54 +0100 Subject: [PATCH 4/6] cleanup, adapt internalDash, remove debug query value --- web/frontend/src/DashPublic.root.svelte | 120 ++---------------- .../src/generic/plots/Roofline.svelte | 7 - web/frontend/src/status/DashInternal.svelte | 79 ++++++------ .../src/status/dashdetails/StatusDash.svelte | 2 +- 4 files changed, 54 insertions(+), 154 deletions(-) diff --git a/web/frontend/src/DashPublic.root.svelte b/web/frontend/src/DashPublic.root.svelte index 2e06012..055e489 100644 --- a/web/frontend/src/DashPublic.root.svelte +++ b/web/frontend/src/DashPublic.root.svelte @@ -16,19 +16,14 @@ } from "@urql/svelte"; import { init, - scramble, - scrambleNames, - convert2uplot } from "./generic/utils.js"; import { - formatDurationTime, formatNumber, } from "./generic/units.js"; import { Row, Col, Card, - CardTitle, CardHeader, CardBody, Spinner, @@ -39,9 +34,10 @@ import Roofline from "./generic/plots/Roofline.svelte"; import Pie, { colors } from "./generic/plots/Pie.svelte"; import Stacked from "./generic/plots/Stacked.svelte"; - // import Histogram from "./generic/plots/Histogram.svelte"; import DoubleMetric from "./generic/plots/DoubleMetricPlot.svelte"; + // Todo: Refresher-Tick + /* Svelte 5 Props */ let { presetCluster, @@ -53,7 +49,6 @@ const useCbColors = getContext("cc-config")?.plotConfiguration_colorblindMode || false /* States */ - let pagingState = $state({page: 1, itemsPerPage: 10}) // Top 10 let from = $state(new Date(Date.now() - (5 * 60 * 1000))); let clusterFrom = $state(new Date(Date.now() - (8 * 60 * 60 * 1000))); let to = $state(new Date(Date.now())); @@ -68,13 +63,8 @@ const statesTimed = $derived(queryStore({ client: client, query: gql` - query ($filter: [NodeFilter!], $typeNode: String!, $typeHealth: String!) { - nodeStates: nodeStatesTimed(filter: $filter, type: $typeNode) { - state - counts - times - } - healthStates: nodeStatesTimed(filter: $filter, type: $typeHealth) { + query ($filter: [NodeFilter!], $type: String!) { + nodeStatesTimed(filter: $filter, type: $type) { state counts times @@ -82,9 +72,8 @@ } `, variables: { - filter: { cluster: { eq: presetCluster }, timeStart: stackedFrom}, // DEBUG VALUE 1760096999, use StackedFrom - typeNode: "node", - typeHealth: "health" + filter: { cluster: { eq: presetCluster }, timeStart: stackedFrom}, + type: "node", }, requestPolicy: "network-only" })); @@ -97,7 +86,6 @@ query ( $cluster: String! $metrics: [String!] - # $nmetrics: [String!] $from: Time! $to: Time! $clusterFrom: Time! @@ -207,7 +195,6 @@ variables: { cluster: presetCluster, metrics: ["flops_any", "mem_bw"], // Metrics For Cluster Plot and Roofline - // nmetrics: ["cpu_load", "acc_utilization"], // Metrics for Node Graph from: from.toISOString(), clusterFrom: clusterFrom.toISOString(), to: to.toISOString(), @@ -219,31 +206,6 @@ requestPolicy: "network-only" })); - const topJobsQuery = $derived(queryStore({ - client: client, - query: gql` - query ( - $filter: [JobFilter!]! - $paging: PageRequest! - ) { - jobsStatistics( - filter: $filter - page: $paging - sortBy: TOTALJOBS - groupBy: PROJECT - ) { - id - totalJobs - } - } - `, - variables: { - filter: [{ state: ["running"] }, { cluster: { eq: presetCluster} }], - paging: pagingState // Top 10 - }, - requestPolicy: "network-only" - })); - // Note: nodeMetrics are requested on configured $timestep resolution const nodeStatusQuery = $derived(queryStore({ client: client, @@ -351,19 +313,6 @@ }); /* Functions */ - // function legendColors(targetIdx, useAltColors) { - // // Reuses first color if targetIdx overflows - // let c; - // if (useCbColors) { - // c = [...colors['colorblind']]; - // } else if (useAltColors) { - // c = [...colors['alternative']]; - // } else { - // c = [...colors['default']]; - // } - // return c[(c.length + targetIdx) % c.length]; - // } - function transformNodesStatsToData(subclusterData) { let data = null const x = [], y = [] @@ -415,30 +364,18 @@ return result } - /* Inspect */ - $inspect(clusterInfo).with((type, clusterInfo) => { - console.log(type, 'clusterInfo', clusterInfo) - }); - - $inspect($statusQuery?.data?.clusterMetrics).with((type, clusterMetrics) => { - console.log(type, 'clusterMetrics', clusterMetrics) - }); - - - {#if $statusQuery.fetching || $statesTimed.fetching || $topJobsQuery.fetching || $nodeStatusQuery.fetching} + {#if $statusQuery.fetching || $statesTimed.fetching || $nodeStatusQuery.fetching} - {:else if $statusQuery.error || $statesTimed.error || $topJobsQuery.error || $nodeStatusQuery.error} + {:else if $statusQuery.error || $statesTimed.error || $nodeStatusQuery.error} {#if $statusQuery.error} @@ -450,11 +387,6 @@ Error Requesting StatesTimed: {$statesTimed.error.message} {/if} - {#if $topJobsQuery.error} - - Error Requesting TopJobsQuery: {$topJobsQuery.error.message} - - {/if} {#if $nodeStatusQuery.error} Error Requesting NodeStatusQuery: {$nodeStatusQuery.error.message} @@ -477,10 +409,6 @@ - @@ -559,7 +487,7 @@ - +
- @@ -620,9 +525,6 @@
{#key refinedStateData} -
- {#key $statesTimed?.data?.nodeStates} + {#key $statesTimed?.data?.nodeStatesTimed} { - console.log(type, 'clusterInfo', clusterInfo) - }); - - - -

{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)} Dashboard

-
- + + {#if $statusQuery.fetching || $statesTimed.fetching || $topJobsQuery.fetching || $nodeStatusQuery.fetching}
@@ -409,7 +422,7 @@ Cluster "{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}" - {[...clusterInfo?.processorTypes].toString()} + {[...clusterInfo?.processorTypes].join(', ')}
@@ -488,6 +501,7 @@ + @@ -529,6 +543,7 @@ +
{#key $statusQuery?.data?.jobsMetricStats} @@ -544,31 +559,20 @@ {/key}
- - {#if clusterInfo?.totalAccs == 0} - +
+ - {:else} - - {/if} +
+
{#key $statesTimed?.data?.nodeStates} @@ -584,6 +588,7 @@ {/key}
+
{#key $statesTimed?.data?.healthStates} diff --git a/web/frontend/src/status/dashdetails/StatusDash.svelte b/web/frontend/src/status/dashdetails/StatusDash.svelte index ee60483..f1f5a1a 100644 --- a/web/frontend/src/status/dashdetails/StatusDash.svelte +++ b/web/frontend/src/status/dashdetails/StatusDash.svelte @@ -83,7 +83,7 @@ } `, variables: { - filter: { cluster: { eq: cluster }, timeStart: 1760096999}, + filter: { cluster: { eq: cluster }, timeStart: stackedFrom}, typeNode: "node", typeHealth: "health" }, From c5aff1a2ca39fc8363be8c9d56070278700ea45e Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Mon, 15 Dec 2025 13:55:02 +0100 Subject: [PATCH 5/6] add autorefresh, remove leftover query --- web/frontend/src/DashPublic.root.svelte | 61 ++++++------------ .../src/generic/helper/Refresher.svelte | 43 +++++++------ web/frontend/src/status/DashInternal.svelte | 64 +++++++------------ .../src/status/dashdetails/StatusDash.svelte | 2 +- 4 files changed, 69 insertions(+), 101 deletions(-) diff --git a/web/frontend/src/DashPublic.root.svelte b/web/frontend/src/DashPublic.root.svelte index 055e489..d3eb57b 100644 --- a/web/frontend/src/DashPublic.root.svelte +++ b/web/frontend/src/DashPublic.root.svelte @@ -35,8 +35,7 @@ import Pie, { colors } from "./generic/plots/Pie.svelte"; import Stacked from "./generic/plots/Stacked.svelte"; import DoubleMetric from "./generic/plots/DoubleMetricPlot.svelte"; - - // Todo: Refresher-Tick + import Refresher from "./generic/helper/Refresher.svelte"; /* Svelte 5 Props */ let { @@ -206,35 +205,6 @@ requestPolicy: "network-only" })); - // Note: nodeMetrics are requested on configured $timestep resolution - const nodeStatusQuery = $derived(queryStore({ - client: client, - query: gql` - query ( - $filter: [JobFilter!]! - $selectedHistograms: [String!] - $numDurationBins: String - ) { - jobsStatistics(filter: $filter, metrics: $selectedHistograms, numDurationBins: $numDurationBins) { - histNumCores { - count - value - } - histNumAccs { - count - value - } - } - } - `, - variables: { - filter: [{ state: ["running"] }, { cluster: { eq: presetCluster } }], - selectedHistograms: [], // No Metrics requested for node hardware stats - Empty Array can be used for refresh - numDurationBins: "1h", // Hardcode or selector? - }, - requestPolicy: "network-only" - })); - const clusterInfo = $derived.by(() => { if ($initq?.data?.clusters) { let rawInfos = {}; @@ -368,28 +338,39 @@ - {#if $statusQuery.fetching || $statesTimed.fetching || $nodeStatusQuery.fetching} + +
+ { + from = new Date(Date.now() - 5 * 60 * 1000); + to = new Date(Date.now()); + clusterFrom = new Date(Date.now() - (8 * 60 * 60 * 1000)) + + if (interval) stackedFrom += Math.floor(interval / 1000); + else stackedFrom += 1 // Workaround: TimeSelection not linked, just trigger new data on manual refresh + }} + /> + + + {#if $statusQuery.fetching || $statesTimed.fetching} - {:else if $statusQuery.error || $statesTimed.error || $nodeStatusQuery.error} + {:else if $statusQuery.error || $statesTimed.error} {#if $statusQuery.error} - Error Requesting StatusQuery: {$statusQuery.error.message} + Error Requesting Status Data: {$statusQuery.error.message} {/if} {#if $statesTimed.error} - Error Requesting StatesTimed: {$statesTimed.error.message} - - {/if} - {#if $nodeStatusQuery.error} - - Error Requesting NodeStatusQuery: {$nodeStatusQuery.error.message} + Error Requesting Node Scheduler States: {$statesTimed.error.message} {/if} diff --git a/web/frontend/src/generic/helper/Refresher.svelte b/web/frontend/src/generic/helper/Refresher.svelte index 7f568bf..ca05bf6 100644 --- a/web/frontend/src/generic/helper/Refresher.svelte +++ b/web/frontend/src/generic/helper/Refresher.svelte @@ -14,6 +14,7 @@ let { initially = null, presetClass = "", + hideSelector = false, onRefresh } = $props(); @@ -36,25 +37,27 @@ }); - - - - - - - - - - + + + + + + + + +{/if} diff --git a/web/frontend/src/status/DashInternal.svelte b/web/frontend/src/status/DashInternal.svelte index 495f998..c6abf06 100644 --- a/web/frontend/src/status/DashInternal.svelte +++ b/web/frontend/src/status/DashInternal.svelte @@ -39,6 +39,7 @@ import Pie, { colors } from "../generic/plots/Pie.svelte"; import Stacked from "../generic/plots/Stacked.svelte"; import DoubleMetric from "../generic/plots/DoubleMetricPlot.svelte"; + import Refresher from "../generic/helper/Refresher.svelte"; /* Svelte 5 Props */ let { @@ -224,35 +225,6 @@ requestPolicy: "network-only" })); - // Note: nodeMetrics are requested on configured $timestep resolution - const nodeStatusQuery = $derived(queryStore({ - client: client, - query: gql` - query ( - $filter: [JobFilter!]! - $selectedHistograms: [String!] - $numDurationBins: String - ) { - jobsStatistics(filter: $filter, metrics: $selectedHistograms, numDurationBins: $numDurationBins) { - histNumCores { - count - value - } - histNumAccs { - count - value - } - } - } - `, - variables: { - filter: [{ state: ["running"] }, { cluster: { eq: presetCluster } }], - selectedHistograms: [], // No Metrics requested for node hardware stats - Empty Array can be used for refresh - numDurationBins: "1h", // Hardcode or selector? - }, - requestPolicy: "network-only" - })); - const clusterInfo = $derived.by(() => { if ($initq?.data?.clusters) { let rawInfos = {}; @@ -385,33 +357,45 @@ - {#if $statusQuery.fetching || $statesTimed.fetching || $topJobsQuery.fetching || $nodeStatusQuery.fetching} + + + { + from = new Date(Date.now() - 5 * 60 * 1000); + to = new Date(Date.now()); + clusterFrom = new Date(Date.now() - (8 * 60 * 60 * 1000)) + pagingState = { page:1, itemsPerPage: 10 }; + + if (interval) stackedFrom += Math.floor(interval / 1000); + else stackedFrom += 1 // Workaround: TimeSelection not linked, just trigger new data on manual refresh + }} + /> + + + {#if $statusQuery.fetching || $statesTimed.fetching || $topJobsQuery.fetching} - {:else if $statusQuery.error || $statesTimed.error || $topJobsQuery.error || $nodeStatusQuery.error} - + {:else if $statusQuery.error || $statesTimed.error || $topJobsQuery.error} + {#if $statusQuery.error} - Error Requesting StatusQuery: {$statusQuery.error.message} + Error Requesting Status Data: {$statusQuery.error.message} {/if} {#if $statesTimed.error} - Error Requesting StatesTimed: {$statesTimed.error.message} + Error Requesting Node Scheduler States: {$statesTimed.error.message} {/if} {#if $topJobsQuery.error} - Error Requesting TopJobsQuery: {$topJobsQuery.error.message} - - {/if} - {#if $nodeStatusQuery.error} - - Error Requesting NodeStatusQuery: {$nodeStatusQuery.error.message} + Error Requesting Jobs By Project: {$topJobsQuery.error.message} {/if} diff --git a/web/frontend/src/status/dashdetails/StatusDash.svelte b/web/frontend/src/status/dashdetails/StatusDash.svelte index f1f5a1a..f858969 100644 --- a/web/frontend/src/status/dashdetails/StatusDash.svelte +++ b/web/frontend/src/status/dashdetails/StatusDash.svelte @@ -402,7 +402,7 @@ to = new Date(Date.now()); if (interval) stackedFrom += Math.floor(interval / 1000); - else stackedFrom += 1 // Workaround: TineSelection not linked, just trigger new data on manual refresh + else stackedFrom += 1 // Workaround: TimeSelection not linked, just trigger new data on manual refresh }} /> From d56b0e93db21d2cff011f2f5afcf43c1cbd3e840 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Mon, 15 Dec 2025 15:10:10 +0100 Subject: [PATCH 6/6] cleanup routes, cleanup root components --- internal/routerConfig/routes.go | 17 +------------ web/frontend/src/DashPublic.root.svelte | 8 +++--- web/frontend/src/Status.root.svelte | 31 +++++++++-------------- web/frontend/src/dashpublic.entrypoint.js | 2 +- web/templates/base.tmpl | 4 +-- web/templates/monitoring/dashboard.tmpl | 2 +- 6 files changed, 21 insertions(+), 43 deletions(-) diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go index 71edeef..c2126cd 100644 --- a/internal/routerConfig/routes.go +++ b/internal/routerConfig/routes.go @@ -120,11 +120,6 @@ func setupClusterStatusRoute(i InfoType, r *http.Request) InfoType { i["id"] = vars["cluster"] i["cluster"] = vars["cluster"] i["displayType"] = "DASHBOARD" - from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") - if from != "" || to != "" { - i["from"] = from - i["to"] = to - } return i } @@ -133,11 +128,6 @@ func setupClusterDetailRoute(i InfoType, r *http.Request) InfoType { i["id"] = vars["cluster"] i["cluster"] = vars["cluster"] i["displayType"] = "DETAILS" - from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") - if from != "" || to != "" { - i["from"] = from - i["to"] = to - } return i } @@ -145,12 +135,7 @@ func setupDashboardRoute(i InfoType, r *http.Request) InfoType { vars := mux.Vars(r) i["id"] = vars["cluster"] i["cluster"] = vars["cluster"] - i["displayType"] = "PUBLIC" - from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") - if from != "" || to != "" { - i["from"] = from - i["to"] = to - } + i["displayType"] = "PUBLIC" // Used in Main Template return i } diff --git a/web/frontend/src/DashPublic.root.svelte b/web/frontend/src/DashPublic.root.svelte index d3eb57b..dac3f9a 100644 --- a/web/frontend/src/DashPublic.root.svelte +++ b/web/frontend/src/DashPublic.root.svelte @@ -6,9 +6,9 @@ --> - - -{#if displayType !== "DASHBOARD" && displayType !== "DETAILS"} +{#if displayType === 'DETAILS'} + +{:else if displayType === 'DASHBOARD'} + +{:else} - Unknown displayList type! + + + Unknown DisplayType for Status View! + + -{:else} - {#if displayStatusDetail} - - - {:else} - - - {/if} {/if} diff --git a/web/frontend/src/dashpublic.entrypoint.js b/web/frontend/src/dashpublic.entrypoint.js index 47287c7..b9e92ff 100644 --- a/web/frontend/src/dashpublic.entrypoint.js +++ b/web/frontend/src/dashpublic.entrypoint.js @@ -5,7 +5,7 @@ import DashPublic from './DashPublic.root.svelte' mount(DashPublic, { target: document.getElementById('svelte-app'), props: { - presetCluster: infos.cluster, + presetCluster: presetCluster, }, context: new Map([ ['cc-config', clusterCockpitConfig] diff --git a/web/templates/base.tmpl b/web/templates/base.tmpl index 28eab33..a1bd413 100644 --- a/web/templates/base.tmpl +++ b/web/templates/base.tmpl @@ -27,13 +27,13 @@
{{block "content-public" .}} - Whoops, you should not see this... [MAIN] + Whoops, you should not see this... [PUBLIC] {{end}}
{{block "javascript-public" .}} - Whoops, you should not see this... [JS] + Whoops, you should not see this... [JS PUBLIC] {{end}} {{else}} diff --git a/web/templates/monitoring/dashboard.tmpl b/web/templates/monitoring/dashboard.tmpl index 06666cd..1ef9455 100644 --- a/web/templates/monitoring/dashboard.tmpl +++ b/web/templates/monitoring/dashboard.tmpl @@ -7,7 +7,7 @@ {{end}} {{define "javascript-public"}}