Merge pull request #325 from ClusterCockpit/add_detailed_nodelist

Add detailed nodelist
This commit is contained in:
Jan Eitzinger 2025-01-28 13:53:57 +01:00 committed by GitHub
commit 4ec9f06114
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 2717 additions and 341 deletions

View File

@ -194,6 +194,15 @@ type NodeMetrics {
metrics: [JobMetricWithName!]! metrics: [JobMetricWithName!]!
} }
type NodesResultList {
items: [NodeMetrics!]!
offset: Int
limit: Int
count: Int
totalNodes: Int
hasNextPage: Boolean
}
type ClusterSupport { type ClusterSupport {
cluster: String! cluster: String!
subClusters: [String!]! subClusters: [String!]!
@ -241,6 +250,7 @@ type Query {
rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]! rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]!
nodeMetrics(cluster: String!, nodes: [String!], scopes: [MetricScope!], metrics: [String!], from: Time!, to: Time!): [NodeMetrics!]! nodeMetrics(cluster: String!, nodes: [String!], scopes: [MetricScope!], metrics: [String!], from: Time!, to: Time!): [NodeMetrics!]!
nodeMetricsList(cluster: String!, subCluster: String!, nodeFilter: String!, scopes: [MetricScope!], metrics: [String!], from: Time!, to: Time!, page: PageRequest, resolution: Int): NodesResultList!
} }
type Mutation { type Mutation {

View File

@ -1419,7 +1419,7 @@ func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request)
rw.Header().Set("Content-Type", "text/plain") rw.Header().Set("Content-Type", "text/plain")
key, value := r.FormValue("key"), r.FormValue("value") key, value := r.FormValue("key"), r.FormValue("value")
fmt.Printf("REST > KEY: %#v\nVALUE: %#v\n", key, value) // fmt.Printf("REST > KEY: %#v\nVALUE: %#v\n", key, value)
if err := repository.GetUserCfgRepo().UpdateConfig(key, value, repository.GetUserFromContext(r.Context())); err != nil { if err := repository.GetUserCfgRepo().UpdateConfig(key, value, repository.GetUserFromContext(r.Context())); err != nil {
http.Error(rw, err.Error(), http.StatusUnprocessableEntity) http.Error(rw, err.Error(), http.StatusUnprocessableEntity)

View File

@ -249,6 +249,15 @@ type ComplexityRoot struct {
SubCluster func(childComplexity int) int SubCluster func(childComplexity int) int
} }
NodesResultList struct {
Count func(childComplexity int) int
HasNextPage func(childComplexity int) int
Items func(childComplexity int) int
Limit func(childComplexity int) int
Offset func(childComplexity int) int
TotalNodes func(childComplexity int) int
}
Query struct { Query struct {
AllocatedNodes func(childComplexity int, cluster string) int AllocatedNodes func(childComplexity int, cluster string) int
Clusters func(childComplexity int) int Clusters func(childComplexity int) int
@ -259,6 +268,7 @@ type ComplexityRoot struct {
JobsFootprints func(childComplexity int, filter []*model.JobFilter, metrics []string) int JobsFootprints func(childComplexity int, filter []*model.JobFilter, metrics []string) int
JobsStatistics func(childComplexity int, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) int JobsStatistics func(childComplexity int, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) int
NodeMetrics func(childComplexity int, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) int NodeMetrics func(childComplexity int, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) int
NodeMetricsList func(childComplexity int, cluster string, subCluster string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) int
RooflineHeatmap func(childComplexity int, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) int RooflineHeatmap func(childComplexity int, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) int
Tags func(childComplexity int) int Tags func(childComplexity int) int
User func(childComplexity int, username string) int User func(childComplexity int, username string) int
@ -385,6 +395,7 @@ type QueryResolver interface {
JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) ([]*model.JobsStatistics, error) JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) ([]*model.JobsStatistics, error)
RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) 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) 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, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error)
} }
type SubClusterResolver interface { type SubClusterResolver interface {
NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error)
@ -1288,6 +1299,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.NodeMetrics.SubCluster(childComplexity), true return e.complexity.NodeMetrics.SubCluster(childComplexity), true
case "NodesResultList.count":
if e.complexity.NodesResultList.Count == nil {
break
}
return e.complexity.NodesResultList.Count(childComplexity), true
case "NodesResultList.hasNextPage":
if e.complexity.NodesResultList.HasNextPage == nil {
break
}
return e.complexity.NodesResultList.HasNextPage(childComplexity), true
case "NodesResultList.items":
if e.complexity.NodesResultList.Items == nil {
break
}
return e.complexity.NodesResultList.Items(childComplexity), true
case "NodesResultList.limit":
if e.complexity.NodesResultList.Limit == nil {
break
}
return e.complexity.NodesResultList.Limit(childComplexity), true
case "NodesResultList.offset":
if e.complexity.NodesResultList.Offset == nil {
break
}
return e.complexity.NodesResultList.Offset(childComplexity), true
case "NodesResultList.totalNodes":
if e.complexity.NodesResultList.TotalNodes == nil {
break
}
return e.complexity.NodesResultList.TotalNodes(childComplexity), true
case "Query.allocatedNodes": case "Query.allocatedNodes":
if e.complexity.Query.AllocatedNodes == nil { if e.complexity.Query.AllocatedNodes == nil {
break break
@ -1386,6 +1439,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.NodeMetrics(childComplexity, args["cluster"].(string), args["nodes"].([]string), args["scopes"].([]schema.MetricScope), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time)), true return e.complexity.Query.NodeMetrics(childComplexity, args["cluster"].(string), args["nodes"].([]string), args["scopes"].([]schema.MetricScope), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time)), true
case "Query.nodeMetricsList":
if e.complexity.Query.NodeMetricsList == nil {
break
}
args, err := ec.field_Query_nodeMetricsList_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.NodeMetricsList(childComplexity, args["cluster"].(string), args["subCluster"].(string), args["nodeFilter"].(string), args["scopes"].([]schema.MetricScope), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time), args["page"].(*model.PageRequest), args["resolution"].(*int)), true
case "Query.rooflineHeatmap": case "Query.rooflineHeatmap":
if e.complexity.Query.RooflineHeatmap == nil { if e.complexity.Query.RooflineHeatmap == nil {
break break
@ -2090,6 +2155,15 @@ type NodeMetrics {
metrics: [JobMetricWithName!]! metrics: [JobMetricWithName!]!
} }
type NodesResultList {
items: [NodeMetrics!]!
offset: Int
limit: Int
count: Int
totalNodes: Int
hasNextPage: Boolean
}
type ClusterSupport { type ClusterSupport {
cluster: String! cluster: String!
subClusters: [String!]! subClusters: [String!]!
@ -2137,6 +2211,7 @@ type Query {
rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]! rooflineHeatmap(filter: [JobFilter!]!, rows: Int!, cols: Int!, minX: Float!, minY: Float!, maxX: Float!, maxY: Float!): [[Float!]!]!
nodeMetrics(cluster: String!, nodes: [String!], scopes: [MetricScope!], metrics: [String!], from: Time!, to: Time!): [NodeMetrics!]! nodeMetrics(cluster: String!, nodes: [String!], scopes: [MetricScope!], metrics: [String!], from: Time!, to: Time!): [NodeMetrics!]!
nodeMetricsList(cluster: String!, subCluster: String!, nodeFilter: String!, scopes: [MetricScope!], metrics: [String!], from: Time!, to: Time!, page: PageRequest, resolution: Int): NodesResultList!
} }
type Mutation { type Mutation {
@ -3112,6 +3187,254 @@ func (ec *executionContext) field_Query_jobs_argsOrder(
return zeroVal, nil return zeroVal, nil
} }
func (ec *executionContext) field_Query_nodeMetricsList_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
arg0, err := ec.field_Query_nodeMetricsList_argsCluster(ctx, rawArgs)
if err != nil {
return nil, err
}
args["cluster"] = arg0
arg1, err := ec.field_Query_nodeMetricsList_argsSubCluster(ctx, rawArgs)
if err != nil {
return nil, err
}
args["subCluster"] = arg1
arg2, err := ec.field_Query_nodeMetricsList_argsNodeFilter(ctx, rawArgs)
if err != nil {
return nil, err
}
args["nodeFilter"] = arg2
arg3, err := ec.field_Query_nodeMetricsList_argsScopes(ctx, rawArgs)
if err != nil {
return nil, err
}
args["scopes"] = arg3
arg4, err := ec.field_Query_nodeMetricsList_argsMetrics(ctx, rawArgs)
if err != nil {
return nil, err
}
args["metrics"] = arg4
arg5, err := ec.field_Query_nodeMetricsList_argsFrom(ctx, rawArgs)
if err != nil {
return nil, err
}
args["from"] = arg5
arg6, err := ec.field_Query_nodeMetricsList_argsTo(ctx, rawArgs)
if err != nil {
return nil, err
}
args["to"] = arg6
arg7, err := ec.field_Query_nodeMetricsList_argsPage(ctx, rawArgs)
if err != nil {
return nil, err
}
args["page"] = arg7
arg8, err := ec.field_Query_nodeMetricsList_argsResolution(ctx, rawArgs)
if err != nil {
return nil, err
}
args["resolution"] = arg8
return args, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsCluster(
ctx context.Context,
rawArgs map[string]interface{},
) (string, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["cluster"]
if !ok {
var zeroVal string
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("cluster"))
if tmp, ok := rawArgs["cluster"]; ok {
return ec.unmarshalNString2string(ctx, tmp)
}
var zeroVal string
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsSubCluster(
ctx context.Context,
rawArgs map[string]interface{},
) (string, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["subCluster"]
if !ok {
var zeroVal string
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("subCluster"))
if tmp, ok := rawArgs["subCluster"]; ok {
return ec.unmarshalNString2string(ctx, tmp)
}
var zeroVal string
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsNodeFilter(
ctx context.Context,
rawArgs map[string]interface{},
) (string, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["nodeFilter"]
if !ok {
var zeroVal string
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("nodeFilter"))
if tmp, ok := rawArgs["nodeFilter"]; ok {
return ec.unmarshalNString2string(ctx, tmp)
}
var zeroVal string
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsScopes(
ctx context.Context,
rawArgs map[string]interface{},
) ([]schema.MetricScope, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["scopes"]
if !ok {
var zeroVal []schema.MetricScope
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("scopes"))
if tmp, ok := rawArgs["scopes"]; ok {
return ec.unmarshalOMetricScope2ᚕgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋpkgᚋschemaᚐMetricScopeᚄ(ctx, tmp)
}
var zeroVal []schema.MetricScope
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsMetrics(
ctx context.Context,
rawArgs map[string]interface{},
) ([]string, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["metrics"]
if !ok {
var zeroVal []string
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("metrics"))
if tmp, ok := rawArgs["metrics"]; ok {
return ec.unmarshalOString2ᚕstringᚄ(ctx, tmp)
}
var zeroVal []string
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsFrom(
ctx context.Context,
rawArgs map[string]interface{},
) (time.Time, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["from"]
if !ok {
var zeroVal time.Time
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("from"))
if tmp, ok := rawArgs["from"]; ok {
return ec.unmarshalNTime2timeᚐTime(ctx, tmp)
}
var zeroVal time.Time
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsTo(
ctx context.Context,
rawArgs map[string]interface{},
) (time.Time, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["to"]
if !ok {
var zeroVal time.Time
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("to"))
if tmp, ok := rawArgs["to"]; ok {
return ec.unmarshalNTime2timeᚐTime(ctx, tmp)
}
var zeroVal time.Time
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsPage(
ctx context.Context,
rawArgs map[string]interface{},
) (*model.PageRequest, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["page"]
if !ok {
var zeroVal *model.PageRequest
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("page"))
if tmp, ok := rawArgs["page"]; ok {
return ec.unmarshalOPageRequest2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐPageRequest(ctx, tmp)
}
var zeroVal *model.PageRequest
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetricsList_argsResolution(
ctx context.Context,
rawArgs map[string]interface{},
) (*int, error) {
// We won't call the directive if the argument is null.
// Set call_argument_directives_with_null to true to call directives
// even if the argument is null.
_, ok := rawArgs["resolution"]
if !ok {
var zeroVal *int
return zeroVal, nil
}
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("resolution"))
if tmp, ok := rawArgs["resolution"]; ok {
return ec.unmarshalOInt2ᚖint(ctx, tmp)
}
var zeroVal *int
return zeroVal, nil
}
func (ec *executionContext) field_Query_nodeMetrics_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Query_nodeMetrics_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -9238,6 +9561,263 @@ func (ec *executionContext) fieldContext_NodeMetrics_metrics(_ context.Context,
return fc, nil return fc, nil
} }
func (ec *executionContext) _NodesResultList_items(ctx context.Context, field graphql.CollectedField, obj *model.NodesResultList) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_NodesResultList_items(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) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Items, 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.NodeMetrics)
fc.Result = res
return ec.marshalNNodeMetrics2ᚕᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐNodeMetricsᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_NodesResultList_items(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "NodesResultList",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "host":
return ec.fieldContext_NodeMetrics_host(ctx, field)
case "subCluster":
return ec.fieldContext_NodeMetrics_subCluster(ctx, field)
case "metrics":
return ec.fieldContext_NodeMetrics_metrics(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type NodeMetrics", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _NodesResultList_offset(ctx context.Context, field graphql.CollectedField, obj *model.NodesResultList) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_NodesResultList_offset(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) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Offset, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*int)
fc.Result = res
return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_NodesResultList_offset(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "NodesResultList",
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) _NodesResultList_limit(ctx context.Context, field graphql.CollectedField, obj *model.NodesResultList) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_NodesResultList_limit(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) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Limit, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*int)
fc.Result = res
return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_NodesResultList_limit(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "NodesResultList",
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) _NodesResultList_count(ctx context.Context, field graphql.CollectedField, obj *model.NodesResultList) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_NodesResultList_count(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) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Count, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*int)
fc.Result = res
return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_NodesResultList_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "NodesResultList",
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) _NodesResultList_totalNodes(ctx context.Context, field graphql.CollectedField, obj *model.NodesResultList) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_NodesResultList_totalNodes(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) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.TotalNodes, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*int)
fc.Result = res
return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_NodesResultList_totalNodes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "NodesResultList",
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) _NodesResultList_hasNextPage(ctx context.Context, field graphql.CollectedField, obj *model.NodesResultList) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_NodesResultList_hasNextPage(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) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.HasNextPage, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*bool)
fc.Result = res
return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_NodesResultList_hasNextPage(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "NodesResultList",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Query_clusters(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query_clusters(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_clusters(ctx, field) fc, err := ec.fieldContext_Query_clusters(ctx, field)
if err != nil { if err != nil {
@ -10024,6 +10604,75 @@ func (ec *executionContext) fieldContext_Query_nodeMetrics(ctx context.Context,
return fc, nil return fc, nil
} }
func (ec *executionContext) _Query_nodeMetricsList(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query_nodeMetricsList(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) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().NodeMetricsList(rctx, fc.Args["cluster"].(string), fc.Args["subCluster"].(string), fc.Args["nodeFilter"].(string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["metrics"].([]string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["page"].(*model.PageRequest), fc.Args["resolution"].(*int))
})
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.NodesResultList)
fc.Result = res
return ec.marshalNNodesResultList2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐNodesResultList(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Query_nodeMetricsList(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 "items":
return ec.fieldContext_NodesResultList_items(ctx, field)
case "offset":
return ec.fieldContext_NodesResultList_offset(ctx, field)
case "limit":
return ec.fieldContext_NodesResultList_limit(ctx, field)
case "count":
return ec.fieldContext_NodesResultList_count(ctx, field)
case "totalNodes":
return ec.fieldContext_NodesResultList_totalNodes(ctx, field)
case "hasNextPage":
return ec.fieldContext_NodesResultList_hasNextPage(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type NodesResultList", 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_nodeMetricsList_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) { func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Query___type(ctx, field) fc, err := ec.fieldContext_Query___type(ctx, field)
if err != nil { if err != nil {
@ -16337,6 +16986,55 @@ func (ec *executionContext) _NodeMetrics(ctx context.Context, sel ast.SelectionS
return out return out
} }
var nodesResultListImplementors = []string{"NodesResultList"}
func (ec *executionContext) _NodesResultList(ctx context.Context, sel ast.SelectionSet, obj *model.NodesResultList) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, nodesResultListImplementors)
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("NodesResultList")
case "items":
out.Values[i] = ec._NodesResultList_items(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "offset":
out.Values[i] = ec._NodesResultList_offset(ctx, field, obj)
case "limit":
out.Values[i] = ec._NodesResultList_limit(ctx, field, obj)
case "count":
out.Values[i] = ec._NodesResultList_count(ctx, field, obj)
case "totalNodes":
out.Values[i] = ec._NodesResultList_totalNodes(ctx, field, obj)
case "hasNextPage":
out.Values[i] = ec._NodesResultList_hasNextPage(ctx, field, obj)
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 queryImplementors = []string{"Query"} var queryImplementors = []string{"Query"}
func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@ -16610,6 +17308,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
} }
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "nodeMetricsList":
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_nodeMetricsList(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) }) out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
case "__type": case "__type":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
@ -18655,6 +19375,20 @@ func (ec *executionContext) marshalNNodeMetrics2ᚖgithubᚗcomᚋClusterCockpit
return ec._NodeMetrics(ctx, sel, v) return ec._NodeMetrics(ctx, sel, v)
} }
func (ec *executionContext) marshalNNodesResultList2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐNodesResultList(ctx context.Context, sel ast.SelectionSet, v model.NodesResultList) graphql.Marshaler {
return ec._NodesResultList(ctx, sel, &v)
}
func (ec *executionContext) marshalNNodesResultList2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐNodesResultList(ctx context.Context, sel ast.SelectionSet, v *model.NodesResultList) 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._NodesResultList(ctx, sel, v)
}
func (ec *executionContext) unmarshalNNullableFloat2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋpkgᚋschemaᚐFloat(ctx context.Context, v interface{}) (schema.Float, error) { func (ec *executionContext) unmarshalNNullableFloat2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋpkgᚋschemaᚐFloat(ctx context.Context, v interface{}) (schema.Float, error) {
var res schema.Float var res schema.Float
err := res.UnmarshalGQL(v) err := res.UnmarshalGQL(v)

View File

@ -148,6 +148,15 @@ type NodeMetrics struct {
Metrics []*JobMetricWithName `json:"metrics"` Metrics []*JobMetricWithName `json:"metrics"`
} }
type NodesResultList struct {
Items []*NodeMetrics `json:"items"`
Offset *int `json:"offset,omitempty"`
Limit *int `json:"limit,omitempty"`
Count *int `json:"count,omitempty"`
TotalNodes *int `json:"totalNodes,omitempty"`
HasNextPage *bool `json:"hasNextPage,omitempty"`
}
type OrderByInput struct { type OrderByInput struct {
Field string `json:"field"` Field string `json:"field"`
Type string `json:"type"` Type string `json:"type"`

View File

@ -437,8 +437,8 @@ func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.Job
// NodeMetrics is the resolver for the nodeMetrics field. // NodeMetrics is the resolver for the nodeMetrics field.
func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) { func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) {
user := repository.GetUserFromContext(ctx) user := repository.GetUserFromContext(ctx)
if user != nil && !user.HasRole(schema.RoleAdmin) { if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
return nil, errors.New("you need to be an administrator for this query") return nil, errors.New("you need to be administrator or support staff for this query")
} }
if metrics == nil { if metrics == nil {
@ -449,7 +449,7 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
data, err := metricDataDispatcher.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx) data, err := metricDataDispatcher.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx)
if err != nil { if err != nil {
log.Warn("Error while loading node data") log.Warn("error while loading node data")
return nil, err return nil, err
} }
@ -459,7 +459,10 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
Host: hostname, Host: hostname,
Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)), Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
} }
host.SubCluster, _ = archive.GetSubClusterByNode(cluster, hostname) host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
if err != nil {
log.Warnf("error in nodeMetrics resolver: %s", err)
}
for metric, scopedMetrics := range metrics { for metric, scopedMetrics := range metrics {
for _, scopedMetric := range scopedMetrics { for _, scopedMetric := range scopedMetrics {
@ -477,6 +480,68 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
return nodeMetrics, nil return nodeMetrics, nil
} }
// NodeMetricsList is the resolver for the nodeMetricsList field.
func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, subCluster string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) {
if resolution == nil { // Load from Config
if config.Keys.EnableResampling != nil {
defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
resolution = &defaultRes
} else { // Set 0 (Loads configured metric timestep)
defaultRes := 0
resolution = &defaultRes
}
}
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)
}
}
data, totalNodes, hasNextPage, err := metricDataDispatcher.LoadNodeListData(cluster, subCluster, nodeFilter, metrics, scopes, *resolution, from, to, page, ctx)
if err != nil {
log.Warn("error while loading node data")
return nil, err
}
nodeMetricsList := make([]*model.NodeMetrics, 0, len(data))
for hostname, metrics := range data {
host := &model.NodeMetrics{
Host: hostname,
Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
}
host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
if err != nil {
log.Warnf("error in nodeMetrics resolver: %s", err)
}
for metric, scopedMetrics := range metrics {
for scope, scopedMetric := range scopedMetrics {
host.Metrics = append(host.Metrics, &model.JobMetricWithName{
Name: metric,
Scope: scope,
Metric: scopedMetric,
})
}
}
nodeMetricsList = append(nodeMetricsList, host)
}
nodeMetricsListResult := &model.NodesResultList{
Items: nodeMetricsList,
TotalNodes: &totalNodes,
HasNextPage: &hasNextPage,
}
return nodeMetricsListResult, nil
}
// NumberOfNodes is the resolver for the numberOfNodes field. // NumberOfNodes is the resolver for the numberOfNodes field.
func (r *subClusterResolver) NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) { func (r *subClusterResolver) NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) {
nodeList, err := archive.ParseNodeList(obj.Nodes) nodeList, err := archive.ParseNodeList(obj.Nodes)

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/metricdata" "github.com/ClusterCockpit/cc-backend/internal/metricdata"
"github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
@ -219,7 +220,7 @@ func LoadAverages(
return nil return nil
} }
// Used for the node/system view. Returns a map of nodes to a map of metrics. // Used for the classic node/system view. Returns a map of nodes to a map of metrics.
func LoadNodeData( func LoadNodeData(
cluster string, cluster string,
metrics, nodes []string, metrics, nodes []string,
@ -254,3 +255,53 @@ func LoadNodeData(
return data, nil return data, nil
} }
func LoadNodeListData(
cluster, subCluster, nodeFilter string,
metrics []string,
scopes []schema.MetricScope,
resolution int,
from, to time.Time,
page *model.PageRequest,
ctx context.Context,
) (map[string]schema.JobData, int, bool, error) {
repo, err := metricdata.GetMetricDataRepo(cluster)
if err != nil {
return nil, 0, false, fmt.Errorf("METRICDATA/METRICDATA > no metric data repository configured for '%s'", cluster)
}
if metrics == nil {
for _, m := range archive.GetCluster(cluster).MetricConfig {
metrics = append(metrics, m.Name)
}
}
data, totalNodes, hasNextPage, err := repo.LoadNodeListData(cluster, subCluster, nodeFilter, metrics, scopes, resolution, from, to, page, ctx)
if err != nil {
if len(data) != 0 {
log.Warnf("partial error: %s", err.Error())
} else {
log.Error("Error while loading node data from metric repository")
return nil, totalNodes, hasNextPage, err
}
}
// NOTE: New StatsSeries will always be calculated as 'min/median/max'
const maxSeriesSize int = 8
for _, jd := range data {
for _, scopes := range jd {
for _, jm := range scopes {
if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
continue
}
jm.AddStatisticsSeries()
}
}
}
if data == nil {
return nil, totalNodes, hasNextPage, fmt.Errorf("METRICDATA/METRICDATA > the metric data repository for '%s' does not support this query", cluster)
}
return data, totalNodes, hasNextPage, nil
}

View File

@ -11,10 +11,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -211,7 +213,6 @@ func (ccms *CCMetricStore) LoadData(
} }
jobMetric, ok := jobData[metric][scope] jobMetric, ok := jobData[metric][scope]
if !ok { if !ok {
jobMetric = &schema.JobMetric{ jobMetric = &schema.JobMetric{
Unit: mc.Unit, Unit: mc.Unit,
@ -235,8 +236,7 @@ func (ccms *CCMetricStore) LoadData(
} }
if res.Avg.IsNaN() || res.Min.IsNaN() || res.Max.IsNaN() { if res.Avg.IsNaN() || res.Min.IsNaN() || res.Max.IsNaN() {
// TODO: use schema.Float instead of float64? // "schema.Float()" because regular float64 can not be JSONed when NaN.
// This is done because regular float64 can not be JSONed when NaN.
res.Avg = schema.Float(0) res.Avg = schema.Float(0)
res.Min = schema.Float(0) res.Min = schema.Float(0)
res.Max = schema.Float(0) res.Max = schema.Float(0)
@ -693,6 +693,445 @@ func (ccms *CCMetricStore) LoadNodeData(
return data, nil return data, nil
} }
func (ccms *CCMetricStore) LoadNodeListData(
cluster, subCluster, nodeFilter string,
metrics []string,
scopes []schema.MetricScope,
resolution int,
from, to time.Time,
page *model.PageRequest,
ctx context.Context,
) (map[string]schema.JobData, int, bool, error) {
// 0) Init additional vars
var totalNodes int = 0
var hasNextPage bool = false
// 1) Get list of all nodes
var nodes []string
if subCluster != "" {
scNodes := archive.NodeLists[cluster][subCluster]
nodes = scNodes.PrintList()
} else {
subClusterNodeLists := archive.NodeLists[cluster]
for _, nodeList := range subClusterNodeLists {
nodes = append(nodes, nodeList.PrintList()...)
}
}
// 2) Filter nodes
if nodeFilter != "" {
filteredNodes := []string{}
for _, node := range nodes {
if strings.Contains(node, nodeFilter) {
filteredNodes = append(filteredNodes, node)
}
}
nodes = filteredNodes
}
// 2.1) Count total nodes && Sort nodes -> Sorting invalidated after ccms return ...
totalNodes = len(nodes)
sort.Strings(nodes)
// 3) Apply paging
if len(nodes) > page.ItemsPerPage {
start := (page.Page - 1) * page.ItemsPerPage
end := start + page.ItemsPerPage
if end > len(nodes) {
end = len(nodes)
hasNextPage = false
} else {
hasNextPage = true
}
nodes = nodes[start:end]
}
// Note: Order of node data is not guaranteed after this point, but contents match page and filter criteria
queries, assignedScope, err := ccms.buildNodeQueries(cluster, subCluster, nodes, metrics, scopes, resolution)
if err != nil {
log.Warn("Error while building queries")
return nil, totalNodes, hasNextPage, err
}
req := ApiQueryRequest{
Cluster: cluster,
Queries: queries,
From: from.Unix(),
To: to.Unix(),
WithStats: true,
WithData: true,
}
resBody, err := ccms.doRequest(ctx, &req)
if err != nil {
log.Error(fmt.Sprintf("Error while performing request %#v\n", err))
return nil, totalNodes, hasNextPage, err
}
var errors []string
data := make(map[string]schema.JobData)
for i, row := range resBody.Results {
var query ApiQuery
if resBody.Queries != nil {
query = resBody.Queries[i]
} else {
query = req.Queries[i]
}
// qdata := res[0]
metric := ccms.toLocalName(query.Metric)
scope := assignedScope[i]
mc := archive.GetMetricConfig(cluster, metric)
res := row[0].Resolution
if res == 0 {
res = mc.Timestep
}
// Init Nested Map Data Structures If Not Found
hostData, ok := data[query.Hostname]
if !ok {
hostData = make(schema.JobData)
data[query.Hostname] = hostData
}
metricData, ok := hostData[metric]
if !ok {
metricData = make(map[schema.MetricScope]*schema.JobMetric)
data[query.Hostname][metric] = metricData
}
scopeData, ok := metricData[scope]
if !ok {
scopeData = &schema.JobMetric{
Unit: mc.Unit,
Timestep: res,
Series: make([]schema.Series, 0),
}
data[query.Hostname][metric][scope] = scopeData
}
for ndx, res := range row {
if res.Error != nil {
/* Build list for "partial errors", if any */
errors = append(errors, fmt.Sprintf("failed to fetch '%s' from host '%s': %s", query.Metric, query.Hostname, *res.Error))
continue
}
id := (*string)(nil)
if query.Type != nil {
id = new(string)
*id = query.TypeIds[ndx]
}
if res.Avg.IsNaN() || res.Min.IsNaN() || res.Max.IsNaN() {
// "schema.Float()" because regular float64 can not be JSONed when NaN.
res.Avg = schema.Float(0)
res.Min = schema.Float(0)
res.Max = schema.Float(0)
}
scopeData.Series = append(scopeData.Series, schema.Series{
Hostname: query.Hostname,
Id: id,
Statistics: schema.MetricStatistics{
Avg: float64(res.Avg),
Min: float64(res.Min),
Max: float64(res.Max),
},
Data: res.Data,
})
}
}
if len(errors) != 0 {
/* Returns list of "partial errors" */
return data, totalNodes, hasNextPage, fmt.Errorf("METRICDATA/CCMS > Errors: %s", strings.Join(errors, ", "))
}
return data, totalNodes, hasNextPage, nil
}
func (ccms *CCMetricStore) buildNodeQueries(
cluster string,
subCluster string,
nodes []string,
metrics []string,
scopes []schema.MetricScope,
resolution int,
) ([]ApiQuery, []schema.MetricScope, error) {
queries := make([]ApiQuery, 0, len(metrics)*len(scopes)*len(nodes))
assignedScope := []schema.MetricScope{}
// Get Topol before loop if subCluster given
var subClusterTopol *schema.SubCluster
var scterr error
if subCluster != "" {
subClusterTopol, scterr = archive.GetSubCluster(cluster, subCluster)
if scterr != nil {
// TODO: Log
return nil, nil, scterr
}
}
for _, metric := range metrics {
remoteName := ccms.toRemoteName(metric)
mc := archive.GetMetricConfig(cluster, metric)
if mc == nil {
// return nil, fmt.Errorf("METRICDATA/CCMS > metric '%s' is not specified for cluster '%s'", metric, cluster)
log.Infof("metric '%s' is not specified for cluster '%s'", metric, cluster)
continue
}
// Avoid duplicates...
handledScopes := make([]schema.MetricScope, 0, 3)
scopesLoop:
for _, requestedScope := range scopes {
nativeScope := mc.Scope
scope := nativeScope.Max(requestedScope)
for _, s := range handledScopes {
if scope == s {
continue scopesLoop
}
}
handledScopes = append(handledScopes, scope)
for _, hostname := range nodes {
// If no subCluster given, get it by node
if subCluster == "" {
subClusterName, scnerr := archive.GetSubClusterByNode(cluster, hostname)
if scnerr != nil {
return nil, nil, scnerr
}
subClusterTopol, scterr = archive.GetSubCluster(cluster, subClusterName)
if scterr != nil {
return nil, nil, scterr
}
}
// Always full node hwthread id list, no partial queries expected -> Use "topology.Node" directly where applicable
// Always full accelerator id list, no partial queries expected -> Use "acceleratorIds" directly where applicable
topology := subClusterTopol.Topology
acceleratorIds := topology.GetAcceleratorIDs()
// Moved check here if metric matches hardware specs
if nativeScope == schema.MetricScopeAccelerator && len(acceleratorIds) == 0 {
continue scopesLoop
}
// Accelerator -> Accelerator (Use "accelerator" scope if requested scope is lower than node)
if nativeScope == schema.MetricScopeAccelerator && scope.LT(schema.MetricScopeNode) {
if scope != schema.MetricScopeAccelerator {
// Skip all other catched cases
continue
}
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: false,
Type: &acceleratorString,
TypeIds: acceleratorIds,
Resolution: resolution,
})
assignedScope = append(assignedScope, schema.MetricScopeAccelerator)
continue
}
// Accelerator -> Node
if nativeScope == schema.MetricScopeAccelerator && scope == schema.MetricScopeNode {
if len(acceleratorIds) == 0 {
continue
}
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: true,
Type: &acceleratorString,
TypeIds: acceleratorIds,
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// HWThread -> HWThead
if nativeScope == schema.MetricScopeHWThread && scope == schema.MetricScopeHWThread {
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: false,
Type: &hwthreadString,
TypeIds: intToStringSlice(topology.Node),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// HWThread -> Core
if nativeScope == schema.MetricScopeHWThread && scope == schema.MetricScopeCore {
cores, _ := topology.GetCoresFromHWThreads(topology.Node)
for _, core := range cores {
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: true,
Type: &hwthreadString,
TypeIds: intToStringSlice(topology.Core[core]),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
}
continue
}
// HWThread -> Socket
if nativeScope == schema.MetricScopeHWThread && scope == schema.MetricScopeSocket {
sockets, _ := topology.GetSocketsFromHWThreads(topology.Node)
for _, socket := range sockets {
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: true,
Type: &hwthreadString,
TypeIds: intToStringSlice(topology.Socket[socket]),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
}
continue
}
// HWThread -> Node
if nativeScope == schema.MetricScopeHWThread && scope == schema.MetricScopeNode {
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: true,
Type: &hwthreadString,
TypeIds: intToStringSlice(topology.Node),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// Core -> Core
if nativeScope == schema.MetricScopeCore && scope == schema.MetricScopeCore {
cores, _ := topology.GetCoresFromHWThreads(topology.Node)
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: false,
Type: &coreString,
TypeIds: intToStringSlice(cores),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// Core -> Node
if nativeScope == schema.MetricScopeCore && scope == schema.MetricScopeNode {
cores, _ := topology.GetCoresFromHWThreads(topology.Node)
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: true,
Type: &coreString,
TypeIds: intToStringSlice(cores),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// MemoryDomain -> MemoryDomain
if nativeScope == schema.MetricScopeMemoryDomain && scope == schema.MetricScopeMemoryDomain {
sockets, _ := topology.GetMemoryDomainsFromHWThreads(topology.Node)
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: false,
Type: &memoryDomainString,
TypeIds: intToStringSlice(sockets),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// MemoryDoman -> Node
if nativeScope == schema.MetricScopeMemoryDomain && scope == schema.MetricScopeNode {
sockets, _ := topology.GetMemoryDomainsFromHWThreads(topology.Node)
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: true,
Type: &memoryDomainString,
TypeIds: intToStringSlice(sockets),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// Socket -> Socket
if nativeScope == schema.MetricScopeSocket && scope == schema.MetricScopeSocket {
sockets, _ := topology.GetSocketsFromHWThreads(topology.Node)
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: false,
Type: &socketString,
TypeIds: intToStringSlice(sockets),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// Socket -> Node
if nativeScope == schema.MetricScopeSocket && scope == schema.MetricScopeNode {
sockets, _ := topology.GetSocketsFromHWThreads(topology.Node)
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Aggregate: true,
Type: &socketString,
TypeIds: intToStringSlice(sockets),
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
// Node -> Node
if nativeScope == schema.MetricScopeNode && scope == schema.MetricScopeNode {
queries = append(queries, ApiQuery{
Metric: remoteName,
Hostname: hostname,
Resolution: resolution,
})
assignedScope = append(assignedScope, scope)
continue
}
return nil, nil, fmt.Errorf("METRICDATA/CCMS > TODO: unhandled case: native-scope=%s, requested-scope=%s", nativeScope, requestedScope)
}
}
}
return queries, assignedScope, nil
}
func intToStringSlice(is []int) []string { func intToStringSlice(is []int) []string {
ss := make([]string, len(is)) ss := make([]string, len(is))
for i, x := range is { for i, x := range is {

View File

@ -13,6 +13,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -312,3 +313,21 @@ func (idb *InfluxDBv2DataRepository) LoadNodeData(
return nil, errors.New("METRICDATA/INFLUXV2 > unimplemented for InfluxDBv2DataRepository") return nil, errors.New("METRICDATA/INFLUXV2 > unimplemented for InfluxDBv2DataRepository")
} }
func (idb *InfluxDBv2DataRepository) LoadNodeListData(
cluster, subCluster, nodeFilter string,
metrics []string,
scopes []schema.MetricScope,
resolution int,
from, to time.Time,
page *model.PageRequest,
ctx context.Context,
) (map[string]schema.JobData, int, bool, error) {
var totalNodes int = 0
var hasNextPage bool = false
// TODO : Implement to be used in NodeList-View
log.Infof("LoadNodeListData unimplemented for InfluxDBv2DataRepository, Args: cluster %s, metrics %v, nodeFilter %v, scopes %v", cluster, metrics, nodeFilter, scopes)
return nil, totalNodes, hasNextPage, errors.New("METRICDATA/INFLUXV2 > unimplemented for InfluxDBv2DataRepository")
}

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
) )
@ -26,8 +27,11 @@ type MetricDataRepository interface {
// Return a map of metrics to a map of nodes to the metric statistics of the job. node scope assumed for now. // Return a map of metrics to a map of nodes to the metric statistics of the job. node scope assumed for now.
LoadStats(job *schema.Job, metrics []string, ctx context.Context) (map[string]map[string]schema.MetricStatistics, error) LoadStats(job *schema.Job, metrics []string, ctx context.Context) (map[string]map[string]schema.MetricStatistics, error)
// Return a map of hosts to a map of metrics at the requested scopes for that node. // Return a map of hosts to a map of metrics at the requested scopes (currently only node) for that node.
LoadNodeData(cluster string, metrics, nodes []string, scopes []schema.MetricScope, from, to time.Time, ctx context.Context) (map[string]map[string][]*schema.JobMetric, error) LoadNodeData(cluster string, metrics, nodes []string, scopes []schema.MetricScope, from, to time.Time, ctx context.Context) (map[string]map[string][]*schema.JobMetric, error)
// Return a map of hosts to a map of metrics to a map of scopes for multiple nodes.
LoadNodeListData(cluster, subCluster, nodeFilter string, metrics []string, scopes []schema.MetricScope, resolution int, from, to time.Time, page *model.PageRequest, ctx context.Context) (map[string]schema.JobData, int, bool, error)
} }
var metricDataRepos map[string]MetricDataRepository = map[string]MetricDataRepository{} var metricDataRepos map[string]MetricDataRepository = map[string]MetricDataRepository{}

View File

@ -20,6 +20,7 @@ import (
"text/template" "text/template"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -446,3 +447,21 @@ func (pdb *PrometheusDataRepository) LoadNodeData(
log.Debugf("LoadNodeData of %v nodes took %s", len(data), t1) log.Debugf("LoadNodeData of %v nodes took %s", len(data), t1)
return data, nil return data, nil
} }
func (pdb *PrometheusDataRepository) LoadNodeListData(
cluster, subCluster, nodeFilter string,
metrics []string,
scopes []schema.MetricScope,
resolution int,
from, to time.Time,
page *model.PageRequest,
ctx context.Context,
) (map[string]schema.JobData, int, bool, error) {
var totalNodes int = 0
var hasNextPage bool = false
// TODO : Implement to be used in NodeList-View
log.Infof("LoadNodeListData unimplemented for PrometheusDataRepository, Args: cluster %s, metrics %v, nodeFilter %v, scopes %v", cluster, metrics, nodeFilter, scopes)
return nil, totalNodes, hasNextPage, errors.New("METRICDATA/INFLUXV2 > unimplemented for PrometheusDataRepository")
}

View File

@ -9,6 +9,7 @@ import (
"encoding/json" "encoding/json"
"time" "time"
"github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
) )
@ -50,6 +51,19 @@ func (tmdr *TestMetricDataRepository) LoadNodeData(
panic("TODO") panic("TODO")
} }
func (tmdr *TestMetricDataRepository) LoadNodeListData(
cluster, subCluster, nodeFilter string,
metrics []string,
scopes []schema.MetricScope,
resolution int,
from, to time.Time,
page *model.PageRequest,
ctx context.Context,
) (map[string]schema.JobData, int, bool, error) {
panic("TODO")
}
func DeepCopy(jd_temp schema.JobData) schema.JobData { func DeepCopy(jd_temp schema.JobData) schema.JobData {
var jd schema.JobData var jd schema.JobData

View File

@ -42,10 +42,12 @@ var routes []Route = []Route{
{"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }}, {"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
{"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute}, {"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute},
{"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute}, {"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute},
{"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute}, {"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> Node Overview - ClusterCockpit", false, setupClusterOverviewRoute},
{"/monitoring/systems/list/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> Node List - ClusterCockpit", false, setupClusterListRoute},
{"/monitoring/systems/list/{cluster}/{subcluster}", "monitoring/systems.tmpl", "Cluster <ID> <SID> Node List - ClusterCockpit", false, setupClusterListRoute},
{"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node <ID> - ClusterCockpit", false, setupNodeRoute}, {"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node <ID> - ClusterCockpit", false, setupNodeRoute},
{"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute}, {"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute},
{"/monitoring/status/{cluster}", "monitoring/status.tmpl", "Status of <ID> - ClusterCockpit", false, setupClusterRoute}, {"/monitoring/status/{cluster}", "monitoring/status.tmpl", "Status of <ID> - ClusterCockpit", false, setupClusterStatusRoute},
} }
func setupHomeRoute(i InfoType, r *http.Request) InfoType { func setupHomeRoute(i InfoType, r *http.Request) InfoType {
@ -111,7 +113,7 @@ func setupUserRoute(i InfoType, r *http.Request) InfoType {
return i return i
} }
func setupClusterRoute(i InfoType, r *http.Request) InfoType { func setupClusterStatusRoute(i InfoType, r *http.Request) InfoType {
vars := mux.Vars(r) vars := mux.Vars(r)
i["id"] = vars["cluster"] i["id"] = vars["cluster"]
i["cluster"] = vars["cluster"] i["cluster"] = vars["cluster"]
@ -123,6 +125,36 @@ func setupClusterRoute(i InfoType, r *http.Request) InfoType {
return i return i
} }
func setupClusterOverviewRoute(i InfoType, r *http.Request) InfoType {
vars := mux.Vars(r)
i["id"] = vars["cluster"]
i["cluster"] = vars["cluster"]
i["displayType"] = "OVERVIEW"
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
if from != "" || to != "" {
i["from"] = from
i["to"] = to
}
return i
}
func setupClusterListRoute(i InfoType, r *http.Request) InfoType {
vars := mux.Vars(r)
i["id"] = vars["cluster"]
i["cluster"] = vars["cluster"]
i["sid"] = vars["subcluster"]
i["subCluster"] = vars["subcluster"]
i["displayType"] = "LIST"
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
if from != "" || to != "" {
i["from"] = from
i["to"] = to
}
return i
}
func setupNodeRoute(i InfoType, r *http.Request) InfoType { func setupNodeRoute(i InfoType, r *http.Request) InfoType {
vars := mux.Vars(r) vars := mux.Vars(r)
i["cluster"] = vars["cluster"] i["cluster"] = vars["cluster"]
@ -343,6 +375,9 @@ func SetupRoutes(router *mux.Router, buildInfo web.Build) {
infos := route.Setup(map[string]interface{}{}, r) infos := route.Setup(map[string]interface{}{}, r)
if id, ok := infos["id"]; ok { if id, ok := infos["id"]; ok {
title = strings.Replace(route.Title, "<ID>", id.(string), 1) title = strings.Replace(route.Title, "<ID>", id.(string), 1)
if sid, ok := infos["sid"]; ok { // 2nd ID element
title = strings.Replace(title, "<SID>", sid.(string), 1)
}
} }
// Get User -> What if NIL? // Get User -> What if NIL?

View File

@ -15,12 +15,12 @@ import (
var ( var (
Clusters []*schema.Cluster Clusters []*schema.Cluster
GlobalMetricList []*schema.GlobalMetricListItem GlobalMetricList []*schema.GlobalMetricListItem
nodeLists map[string]map[string]NodeList NodeLists map[string]map[string]NodeList
) )
func initClusterConfig() error { func initClusterConfig() error {
Clusters = []*schema.Cluster{} Clusters = []*schema.Cluster{}
nodeLists = map[string]map[string]NodeList{} NodeLists = map[string]map[string]NodeList{}
metricLookup := make(map[string]schema.GlobalMetricListItem) metricLookup := make(map[string]schema.GlobalMetricListItem)
for _, c := range ar.GetClusters() { for _, c := range ar.GetClusters() {
@ -109,7 +109,7 @@ func initClusterConfig() error {
Clusters = append(Clusters, cluster) Clusters = append(Clusters, cluster)
nodeLists[cluster.Name] = make(map[string]NodeList) NodeLists[cluster.Name] = make(map[string]NodeList)
for _, sc := range cluster.SubClusters { for _, sc := range cluster.SubClusters {
if sc.Nodes == "*" { if sc.Nodes == "*" {
continue continue
@ -119,7 +119,7 @@ func initClusterConfig() error {
if err != nil { if err != nil {
return fmt.Errorf("ARCHIVE/CLUSTERCONFIG > in %s/cluster.json: %w", cluster.Name, err) return fmt.Errorf("ARCHIVE/CLUSTERCONFIG > in %s/cluster.json: %w", cluster.Name, err)
} }
nodeLists[cluster.Name][sc.Name] = nl NodeLists[cluster.Name][sc.Name] = nl
} }
} }
@ -187,7 +187,7 @@ func AssignSubCluster(job *schema.BaseJob) error {
} }
host0 := job.Resources[0].Hostname host0 := job.Resources[0].Hostname
for sc, nl := range nodeLists[job.Cluster] { for sc, nl := range NodeLists[job.Cluster] {
if nl != nil && nl.Contains(host0) { if nl != nil && nl.Contains(host0) {
job.SubCluster = sc job.SubCluster = sc
return nil return nil
@ -203,7 +203,7 @@ func AssignSubCluster(job *schema.BaseJob) error {
} }
func GetSubClusterByNode(cluster, hostname string) (string, error) { func GetSubClusterByNode(cluster, hostname string) (string, error) {
for sc, nl := range nodeLists[cluster] { for sc, nl := range NodeLists[cluster] {
if nl != nil && nl.Contains(hostname) { if nl != nil && nl.Contains(hostname) {
return sc, nil return sc, nil
} }

View File

@ -194,7 +194,17 @@ func (topo *Topology) GetAcceleratorID(id int) (string, error) {
} }
} }
func (topo *Topology) GetAcceleratorIDs() ([]int, error) { // Return list of hardware (string) accelerator IDs
func (topo *Topology) GetAcceleratorIDs() []string {
accels := make([]string, 0)
for _, accel := range topo.Accelerators {
accels = append(accels, accel.ID)
}
return accels
}
// Outdated? Or: Return indices of accelerators in parent array?
func (topo *Topology) GetAcceleratorIDsAsInt() ([]int, error) {
accels := make([]int, 0) accels := make([]int, 0)
for _, accel := range topo.Accelerators { for _, accel := range topo.Accelerators {
id, err := strconv.Atoi(accel.ID) id, err := strconv.Atoi(accel.ID)

View File

@ -1,12 +1,12 @@
{ {
"name": "cc-frontend", "name": "cc-frontend",
"version": "1.0.2", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cc-frontend", "name": "cc-frontend",
"version": "1.0.2", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-replace": "^5.0.7",

View File

@ -522,7 +522,6 @@
<Col> <Col>
<PlotGrid <PlotGrid
let:item let:item
renderFor="analysis"
items={metricsInHistograms.map((metric) => ({ items={metricsInHistograms.map((metric) => ({
metric, metric,
...binsFromFootprint( ...binsFromFootprint(
@ -566,7 +565,6 @@
<PlotGrid <PlotGrid
let:item let:item
let:width let:width
renderFor="analysis"
items={metricsInScatterplots.map(([m1, m2]) => ({ items={metricsInScatterplots.map(([m1, m2]) => ({
m1, m1,
f1: $footprintsQuery.data.footprints.metrics.find( f1: $footprintsQuery.data.footprints.metrics.find(

View File

@ -3,6 +3,7 @@
Properties: Properties:
- `ìsAdmin Bool!`: Is currently logged in user admin authority - `ìsAdmin Bool!`: Is currently logged in user admin authority
- `isSupport Bool!`: Is currently logged in user support authority
- `isApi Bool!`: Is currently logged in user api authority - `isApi Bool!`: Is currently logged in user api authority
- `username String!`: Empty string if auth. is disabled, otherwise the username as string - `username String!`: Empty string if auth. is disabled, otherwise the username as string
--> -->
@ -10,15 +11,17 @@
<script> <script>
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap"; import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
import UserSettings from "./config/UserSettings.svelte"; import UserSettings from "./config/UserSettings.svelte";
import SupportSettings from "./config/SupportSettings.svelte";
import AdminSettings from "./config/AdminSettings.svelte"; import AdminSettings from "./config/AdminSettings.svelte";
export let isAdmin; export let isAdmin;
export let isSupport;
export let isApi; export let isApi;
export let username; export let username;
export let ncontent; export let ncontent;
</script> </script>
{#if isAdmin == true} {#if isAdmin}
<Card style="margin-bottom: 1.5em;"> <Card style="margin-bottom: 1.5em;">
<CardHeader> <CardHeader>
<CardTitle class="mb-1">Admin Options</CardTitle> <CardTitle class="mb-1">Admin Options</CardTitle>
@ -27,6 +30,15 @@
</Card> </Card>
{/if} {/if}
{#if isSupport || isAdmin}
<Card style="margin-bottom: 1.5em;">
<CardHeader>
<CardTitle class="mb-1">Support Options</CardTitle>
</CardHeader>
<SupportSettings/>
</Card>
{/if}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="mb-1">User Options</CardTitle> <CardTitle class="mb-1">User Options</CardTitle>

View File

@ -26,6 +26,7 @@
export let username; export let username;
export let authlevel; export let authlevel;
export let clusters; export let clusters;
export let subClusters;
export let roles; export let roles;
let isOpen = false; let isOpen = false;
@ -93,10 +94,19 @@
}, },
{ {
title: "Nodes", title: "Nodes",
requiredRole: roles.admin, requiredRole: roles.support,
href: "/monitoring/systems/", href: "/monitoring/systems/",
icon: "hdd-rack", icon: "hdd-rack",
perCluster: true, perCluster: true,
listOptions: true,
menu: "Info",
},
{
title: "Analysis",
requiredRole: roles.support,
href: "/monitoring/analysis/",
icon: "graph-up",
perCluster: true,
listOptions: false, listOptions: false,
menu: "Info", menu: "Info",
}, },
@ -109,15 +119,6 @@
listOptions: false, listOptions: false,
menu: "Info", menu: "Info",
}, },
{
title: "Analysis",
requiredRole: roles.support,
href: "/monitoring/analysis/",
icon: "graph-up",
perCluster: true,
listOptions: false,
menu: "Info",
},
]; ];
</script> </script>
@ -138,11 +139,13 @@
{#if screenSize > 1500 || screenSize < 768} {#if screenSize > 1500 || screenSize < 768}
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters}
links={views.filter((item) => item.requiredRole <= authlevel)} links={views.filter((item) => item.requiredRole <= authlevel)}
/> />
{:else if screenSize > 1300} {:else if screenSize > 1300}
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters}
links={views.filter( links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu != "Info", (item) => item.requiredRole <= authlevel && item.menu != "Info",
)} )}
@ -156,6 +159,7 @@
<DropdownMenu class="dropdown-menu-lg-end"> <DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters}
direction="right" direction="right"
links={views.filter( links={views.filter(
(item) => (item) =>
@ -168,6 +172,7 @@
{:else} {:else}
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters}
links={views.filter( links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == "none", (item) => item.requiredRole <= authlevel && item.menu == "none",
)} )}
@ -180,6 +185,7 @@
<DropdownMenu class="dropdown-menu-lg-end"> <DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters}
direction="right" direction="right"
links={views.filter( links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == 'Jobs', (item) => item.requiredRole <= authlevel && item.menu == 'Jobs',
@ -196,6 +202,7 @@
<DropdownMenu class="dropdown-menu-lg-end"> <DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters}
direction="right" direction="right"
links={views.filter( links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == 'Groups', (item) => item.requiredRole <= authlevel && item.menu == 'Groups',
@ -212,6 +219,7 @@
<DropdownMenu class="dropdown-menu-lg-end"> <DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters}
direction="right" direction="right"
links={views.filter( links={views.filter(
(item) => item.requiredRole <= authlevel && item.menu == 'Info', (item) => item.requiredRole <= authlevel && item.menu == 'Info',

View File

@ -348,7 +348,6 @@
{:else if $initq?.data && $jobMetrics?.data?.jobMetrics} {:else if $initq?.data && $jobMetrics?.data?.jobMetrics}
<PlotGrid <PlotGrid
let:item let:item
renderFor="job"
items={orderAndMap( items={orderAndMap(
groupByScope($jobMetrics.data.jobMetrics), groupByScope($jobMetrics.data.jobMetrics),
selectedMetrics, selectedMetrics,

View File

@ -44,7 +44,7 @@
if (from == null || to == null) { if (from == null || to == null) {
to = new Date(Date.now()); to = new Date(Date.now());
from = new Date(to.getTime()); from = new Date(to.getTime());
from.setHours(from.getHours() - 12); from.setHours(from.getHours() - 4);
} }
const initialized = getContext("initialized") const initialized = getContext("initialized")
@ -141,7 +141,7 @@
<InputGroup> <InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText> <InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Selected Node</InputGroupText> <InputGroupText>Selected Node</InputGroupText>
<Input style="background-color: white;"type="text" value="{hostname} ({cluster})" disabled/> <Input style="background-color: white;"type="text" value="{hostname} [{cluster} ({$nodeMetricsData?.data ? $nodeMetricsData.data.nodeMetrics[0].subCluster : ''})]" disabled/>
</InputGroup> </InputGroup>
</Col> </Col>
<!-- Time Col --> <!-- Time Col -->
@ -162,9 +162,11 @@
</a> </a>
</InputGroup> </InputGroup>
{:else} {:else}
<Input type="text" disabled> <InputGroup>
No currently running jobs. <InputGroupText><Icon name="activity" /></InputGroupText>
</Input> <InputGroupText>Activity</InputGroupText>
<Input type="text" value="No running jobs." disabled />
</InputGroup>
{/if} {/if}
</Col> </Col>
<!-- Refresh Col--> <!-- Refresh Col-->
@ -189,7 +191,6 @@
{:else} {:else}
<PlotGrid <PlotGrid
let:item let:item
renderFor="node"
itemsPerRow={ccconfig.plot_view_plotsPerRow} itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodeMetricsData.data.nodeMetrics[0].metrics items={$nodeMetricsData.data.nodeMetrics[0].metrics
.map((m) => ({ .map((m) => ({

View File

@ -655,7 +655,6 @@
{#key $mainQuery.data.stats[0].histMetrics} {#key $mainQuery.data.stats[0].histMetrics}
<PlotGrid <PlotGrid
let:item let:item
renderFor="user"
items={$mainQuery.data.stats[0].histMetrics} items={$mainQuery.data.stats[0].histMetrics}
itemsPerRow={2} itemsPerRow={2}
> >

View File

@ -1,7 +1,8 @@
<!-- <!--
@component Main cluster metric status view component; renders current state of metrics / nodes @component Main cluster node status view component; renders overview or list depending on type
Properties: Properties:
- `displayType String?`: The type of node display ['OVERVIEW' || 'LIST']
- `cluster String`: The cluster to show status information for - `cluster String`: The cluster to show status information for
- `from Date?`: Custom Time Range selection 'from' [Default: null] - `from Date?`: Custom Time Range selection 'from' [Default: null]
- `to Date?`: Custom Time Range selection 'to' [Default: null] - `to Date?`: Custom Time Range selection 'to' [Default: null]
@ -12,33 +13,34 @@
import { import {
Row, Row,
Col, Col,
Card,
Input, Input,
InputGroup, InputGroup,
InputGroupText, InputGroupText,
Icon, Icon,
Spinner, Button,
Card,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import {
queryStore, import { init } from "./generic/utils.js";
gql, import NodeOverview from "./systems/NodeOverview.svelte";
getContextClient, import NodeList from "./systems/NodeList.svelte";
} from "@urql/svelte"; import MetricSelection from "./generic/select/MetricSelection.svelte";
import {
init,
checkMetricDisabled,
} from "./generic/utils.js";
import PlotGrid from "./generic/PlotGrid.svelte";
import MetricPlot from "./generic/plots/MetricPlot.svelte";
import TimeSelection from "./generic/select/TimeSelection.svelte"; import TimeSelection from "./generic/select/TimeSelection.svelte";
import Refresher from "./generic/helper/Refresher.svelte"; import Refresher from "./generic/helper/Refresher.svelte";
export let displayType;
export let cluster; export let cluster;
export let subCluster = "";
export let from = null; export let from = null;
export let to = null; export let to = null;
const { query: initq } = init(); const { query: initq } = init();
console.assert(
displayType == "OVERVIEW" || displayType == "LIST",
"Invalid nodes displayType provided!",
);
if (from == null || to == null) { if (from == null || to == null) {
to = new Date(Date.now()); to = new Date(Date.now());
from = new Date(to.getTime()); from = new Date(to.getTime());
@ -47,57 +49,28 @@
const initialized = getContext("initialized"); const initialized = getContext("initialized");
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
const clusters = getContext("clusters");
const globalMetrics = getContext("globalMetrics"); const globalMetrics = getContext("globalMetrics");
const displayNodeOverview = (displayType === 'OVERVIEW')
const resampleConfig = getContext("resampling") || null;
const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
let selectedResolution = resampleConfig ? resampleDefault : 0;
let hostnameFilter = ""; let hostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric; let pendingHostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric || "";
let selectedMetrics = ccconfig[`node_list_selectedMetrics:${cluster}`] || [ccconfig.system_view_selectedMetric];
let isMetricsSelectionOpen = false;
const client = getContextClient(); /*
$: nodesQuery = queryStore({ Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
client: client, Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
query: gql` */
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(
cluster: $cluster
metrics: $metrics
from: $from
to: $to
) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit {
base
prefix
}
series {
statistics {
min
avg
max
}
data
}
}
}
}
}
`,
variables: {
cluster: cluster,
metrics: [selectedMetric],
from: from.toISOString(),
to: to.toISOString(),
},
});
let systemMetrics = []; let systemMetrics = [];
let systemUnits = {}; let systemUnits = {};
function loadMetrics(isInitialized) { function loadMetrics(isInitialized) {
if (!isInitialized) return if (!isInitialized) return
systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
@ -108,23 +81,60 @@
$: loadMetrics($initialized) $: loadMetrics($initialized)
$: if (displayNodeOverview) {
selectedMetrics = [selectedMetric]
}
$: { // Wait after input for some time to prevent too many requests
setTimeout(function () {
hostnameFilter = pendingHostnameFilter;
}, 500);
}
</script> </script>
<Row cols={{ xs: 2, lg: 4 }}> <!-- ROW1: Tools-->
{#if $initq.error} <Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 5 : 4) : 4 }} class="mb-3">
<Card body color="danger">{$initq.error.message}</Card> {#if $initq.data}
{:else if $initq.fetching} <!-- List Metric Select Col-->
<Spinner /> {#if !displayNodeOverview}
{:else}
<!-- Node Col-->
<Col> <Col>
<InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText class="text-capitalize">Metrics</InputGroupText>
<Button
outline
color="primary"
on:click={() => (isMetricsSelectionOpen = true)}
>
{selectedMetrics.length} selected
</Button>
</InputGroup>
</Col>
{#if resampleConfig}
<Col>
<InputGroup>
<InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>
<InputGroupText>Resolution</InputGroupText>
<Input type="select" bind:value={selectedResolution}>
{#each resampleResolutions as res}
<option value={res}
>{res} sec</option
>
{/each}
</Input>
</InputGroup>
</Col>
{/if}
{/if}
<!-- Node Col-->
<Col class="mt-2 mt-lg-0">
<InputGroup> <InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText> <InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node</InputGroupText> <InputGroupText>Find Node(s)</InputGroupText>
<Input <Input
placeholder="hostname..." placeholder="Filter hostname ..."
type="text" type="text"
bind:value={hostnameFilter} bind:value={pendingHostnameFilter}
/> />
</InputGroup> </InputGroup>
</Col> </Col>
@ -132,20 +142,22 @@
<Col> <Col>
<TimeSelection bind:from bind:to /> <TimeSelection bind:from bind:to />
</Col> </Col>
<!-- Metric Col--> <!-- Overview Metric Col-->
{#if displayNodeOverview}
<Col class="mt-2 mt-lg-0"> <Col class="mt-2 mt-lg-0">
<InputGroup> <InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText> <InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText> <InputGroupText>Metric</InputGroupText>
<select class="form-select" bind:value={selectedMetric}> <Input type="select" bind:value={selectedMetric}>
{#each systemMetrics as metric} {#each systemMetrics as metric}
<option value={metric.name} <option value={metric.name}
>{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option >{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
> >
{/each} {/each}
</select> </Input>
</InputGroup> </InputGroup>
</Col> </Col>
{/if}
<!-- Refresh Col--> <!-- Refresh Col-->
<Col class="mt-2 mt-lg-0"> <Col class="mt-2 mt-lg-0">
<Refresher <Refresher
@ -158,75 +170,30 @@
</Col> </Col>
{/if} {/if}
</Row> </Row>
<br />
{#if $nodesQuery.error} <!-- ROW2: Content-->
{#if displayType !== "OVERVIEW" && displayType !== "LIST"}
<Row> <Row>
<Col> <Col>
<Card body color="danger">{$nodesQuery.error.message}</Card> <Card body color="danger">Unknown displayList type! </Card>
</Col>
</Row>
{:else if $nodesQuery.fetching || $initq.fetching}
<Row>
<Col>
<Spinner />
</Col> </Col>
</Row> </Row>
{:else} {:else}
<PlotGrid {#if displayNodeOverview}
let:item <!-- ROW2-1: Node Overview (Grid Included)-->
renderFor="systems" <NodeOverview {cluster} {subCluster} {ccconfig} {selectedMetrics} {from} {to} {hostnameFilter}/>
itemsPerRow={ccconfig.plot_view_plotsPerRow} {:else}
items={$nodesQuery.data.nodeMetrics <!-- ROW2-2: Node List (Grid Included)-->
.filter( <NodeList {cluster} {subCluster} {ccconfig} {selectedMetrics} {selectedResolution} {hostnameFilter} {from} {to} {systemUnits}/>
(h) => {/if}
h.host.includes(hostnameFilter) && {/if}
h.metrics.some(
(m) => m.name == selectedMetric && m.scope == "node", <MetricSelection
), {cluster}
) configName="node_list_selectedMetrics"
.map((h) => ({ metrics={selectedMetrics}
host: h.host, bind:isOpen={isMetricsSelectionOpen}
subCluster: h.subCluster, on:update-metrics={({ detail }) => {
data: h.metrics.find( selectedMetrics = [...detail]
(m) => m.name == selectedMetric && m.scope == "node", }}
),
disabled: checkMetricDisabled(
selectedMetric,
cluster,
h.subCluster,
),
}))
.sort((a, b) => a.host.localeCompare(b.host))}
>
<h4 style="width: 100%; text-align: center;">
<a
style="display: block;padding-top: 15px;"
href="/monitoring/node/{cluster}/{item.host}"
>{item.host} ({item.subCluster})</a
>
</h4>
{#if item.disabled === false && item.data}
<MetricPlot
timestep={item.data.metric.timestep}
series={item.data.metric.series}
metric={item.data.name}
cluster={clusters.find((c) => c.name == cluster)}
subCluster={item.subCluster}
forNode={true}
/> />
{:else if item.disabled === true && item.data}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
>Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code
></Card
>
{:else}
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="warning"
>No dataset returned for <code>{selectedMetric}</code></Card
>
{/if}
</PlotGrid>
{/if}

View File

@ -308,7 +308,6 @@
{#key $stats.data.jobsStatistics[0].histMetrics} {#key $stats.data.jobsStatistics[0].histMetrics}
<PlotGrid <PlotGrid
let:item let:item
renderFor="user"
items={$stats.data.jobsStatistics[0].histMetrics} items={$stats.data.jobsStatistics[0].histMetrics}
itemsPerRow={3} itemsPerRow={3}
> >

View File

@ -5,6 +5,7 @@ new Config({
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
isAdmin: isAdmin, isAdmin: isAdmin,
isSupport: isSupport,
isApi: isApi, isApi: isApi,
username: username, username: username,
ncontent: ncontent, ncontent: ncontent,

View File

@ -4,7 +4,7 @@
<script> <script>
import { Row, Col } from "@sveltestrap/sveltestrap"; import { Row, Col } from "@sveltestrap/sveltestrap";
import { onMount } from "svelte"; import { onMount, getContext } from "svelte";
import EditRole from "./admin/EditRole.svelte"; import EditRole from "./admin/EditRole.svelte";
import EditProject from "./admin/EditProject.svelte"; import EditProject from "./admin/EditProject.svelte";
import AddUser from "./admin/AddUser.svelte"; import AddUser from "./admin/AddUser.svelte";
@ -17,6 +17,8 @@
let users = []; let users = [];
let roles = []; let roles = [];
const ccconfig = getContext("cc-config");
function getUserList() { function getUserList() {
fetch("/config/users/?via-ldap=false&not-just-user=true") fetch("/config/users/?via-ldap=false&not-just-user=true")
.then((res) => res.json()) .then((res) => res.json())
@ -54,6 +56,6 @@
<Col> <Col>
<EditProject on:reload={getUserList} /> <EditProject on:reload={getUserList} />
</Col> </Col>
<Options /> <Options config={ccconfig}/>
<NoticeEdit {ncontent}/> <NoticeEdit {ncontent}/>
</Row> </Row>

View File

@ -0,0 +1,13 @@
<!--
@component Support settings wrapper
Properties: None
-->
<script>
import { getContext } from "svelte";
import SupportOptions from "./support/SupportOptions.svelte";
const ccconfig = getContext("cc-config");
</script>
<SupportOptions config={ccconfig}/>

View File

@ -45,7 +45,7 @@
<Col> <Col>
<Card class="h-100"> <Card class="h-100">
<CardBody> <CardBody>
<CardTitle class="mb-3">Metric Plot Resampling</CardTitle> <CardTitle class="mb-3">Metric Plot Resampling Info</CardTitle>
<p>Triggered at {resampleConfig.trigger} datapoints.</p> <p>Triggered at {resampleConfig.trigger} datapoints.</p>
<p>Configured resolutions: {resampleConfig.resolutions}</p> <p>Configured resolutions: {resampleConfig.resolutions}</p>
</CardBody> </CardBody>

View File

@ -0,0 +1,89 @@
<!--
@component Support option select card
-->
<script>
import { Row, Col, Card, CardTitle, Button} from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition";
export let config;
let message;
let displayMessage;
async function handleSettingSubmit(selector, target) {
let form = document.querySelector(selector);
let formData = new FormData(form);
try {
const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) {
let text = await res.text();
popMessage(text, target, "#048109");
} else {
let text = await res.text();
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, target, "#d63384");
}
return false;
}
function popMessage(response, restarget, rescolor) {
message = { msg: response, target: restarget, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
</script>
<Row cols={1} class="p-2 g-2">
<Col>
<Card class="h-100">
<form
id="node-paging-form"
method="post"
action="/frontend/configuration/"
class="card-body"
on:submit|preventDefault={() =>
handleSettingSubmit("#node-paging-form", "npag")}
>
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Node List Paging Type</div>
{#if displayMessage && message.target == "npag"}<div
style="margin-left: auto; font-size: 0.9em;"
>
<code style="color: {message.color};" out:fade
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="node_list_usePaging" />
<div class="mb-3">
<div>
{#if config?.node_list_usePaging}
<input type="radio" id="nodes-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="nodes-true" name="value" value="true" />
{/if}
<label for="true">Paging with selectable count of nodes.</label>
</div>
<div>
{#if config?.node_list_usePaging}
<input type="radio" id="nodes-false" name="value" value="false" />
{:else}
<input type="radio" id="nodes-false-checked" name="value" value="false" checked />
{/if}
<label for="false">Continuous scroll iteratively adding 10 nodes.</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card>
</Col>
</Row>

View File

@ -74,7 +74,7 @@
<CardTitle <CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;" style="margin-bottom: 1em; display: flex; align-items: center;"
> >
<div>Paging Type</div> <div>Job List Paging Type</div>
{#if displayMessage && message.target == "pag"}<div {#if displayMessage && message.target == "pag"}<div
style="margin-left: auto; font-size: 0.9em;" style="margin-left: auto; font-size: 0.9em;"
> >

View File

@ -4,7 +4,6 @@
Properties: Properties:
- `itemsPerRow Number`: Elements to render per row - `itemsPerRow Number`: Elements to render per row
- `items [Any]`: List of plot components to render - `items [Any]`: List of plot components to render
- `renderFor String`: If 'job', filter disabled metrics
--> -->
<script> <script>
@ -15,43 +14,13 @@
export let itemsPerRow export let itemsPerRow
export let items export let items
export let renderFor
let rows = [];
const isPlaceholder = x => x._is_placeholder === true;
function tile(items, itemsPerRow) {
const rows = []
for (let ri = 0; ri < items.length; ri += itemsPerRow) {
const row = []
for (let ci = 0; ci < itemsPerRow; ci += 1) {
if (ri + ci < items.length)
row.push(items[ri + ci])
else
row.push({ _is_placeholder: true, ri, ci })
}
rows.push(row)
}
return rows
}
$: if (renderFor === 'job') {
rows = tile(items.filter(item => item.disabled === false), itemsPerRow)
} else {
rows = tile(items, itemsPerRow)
}
</script> </script>
{#each rows as row} <Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}>
<Row cols={{ xs: 1, sm: 1, md: 2, lg: itemsPerRow}}> {#each items as item}
{#each row as item (item)}
<Col class="px-1"> <Col class="px-1">
{#if !isPlaceholder(item)} <slot {item}/>
<slot item={item}/>
{/if}
</Col> </Col>
{/each} {/each}
</Row> </Row>
{/each}

View File

@ -9,12 +9,12 @@
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `timestep Number`: The timestep used for X-axis rendering - `timestep Number`: The timestep used for X-axis rendering
- `series [GraphQL.Series]`: The metric data object - `series [GraphQL.Series]`: The metric data object
- `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: null] - `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: false]
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null] - `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
- `cluster GraphQL.Cluster`: Cluster Object of the parent job - `cluster String`: Cluster name of the parent job / data
- `subCluster String`: Name of the subCluster of the parent job - `subCluster String`: Name of the subCluster of the parent job
- `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly [Default: false] - `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly [Default: false]
- `forNode Bool?`: If this plot is used for node data display; will ren[data, err := metricdata.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx)](https://github.com/ClusterCockpit/cc-backend/blob/9fe7cdca9215220a19930779a60c8afc910276a3/internal/graph/schema.resolvers.go#L391-L392)der x-axis as negative time with $now as maximum [Default: false] - `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false]
- `numhwthreads Number?`: Number of job HWThreads [Default: 0] - `numhwthreads Number?`: Number of job HWThreads [Default: 0]
- `numaccs Number?`: Number of job Accelerators [Default: 0] - `numaccs Number?`: Number of job Accelerators [Default: 0]
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null] - `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
@ -124,13 +124,13 @@
export let metric; export let metric;
export let scope = "node"; export let scope = "node";
export let width = null; export let width = 0;
export let height = 300; export let height = 300;
export let timestep; export let timestep;
export let series; export let series;
export let useStatsSeries = null; export let useStatsSeries = false;
export let statisticsSeries = null; export let statisticsSeries = null;
export let cluster; export let cluster = "";
export let subCluster; export let subCluster;
export let isShared = false; export let isShared = false;
export let forNode = false; export let forNode = false;
@ -138,11 +138,11 @@
export let numaccs = 0; export let numaccs = 0;
export let zoomState = null; export let zoomState = null;
export let thresholdState = null; export let thresholdState = null;
export let extendedLegendData = null;
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null; if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
if (useStatsSeries == false && series == null) useStatsSeries = true;
const usesMeanStatsSeries = (useStatsSeries && statisticsSeries.mean.length != 0) const usesMeanStatsSeries = (statisticsSeries?.mean && statisticsSeries.mean.length != 0)
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster); const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric); const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
@ -194,6 +194,7 @@
className && legendEl.classList.add(className); className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, { uPlot.assign(legendEl.style, {
minWidth: extendedLegendData ? "300px" : "100px",
textAlign: "left", textAlign: "left",
pointerEvents: "none", pointerEvents: "none",
display: "none", display: "none",
@ -207,11 +208,10 @@
// conditional hide series color markers: // conditional hide series color markers:
if ( if (
useStatsSeries === true || // Min/Max/Median Self-Explanatory useStatsSeries || // Min/Max/Median Self-Explanatory
dataSize === 1 || // Only one Y-Dataseries dataSize === 1 || // Only one Y-Dataseries
dataSize > 6 dataSize > 8 // More than 8 Y-Dataseries
) { ) {
// More than 6 Y-Dataseries
const idents = legendEl.querySelectorAll(".u-marker"); const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++) for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none"; idents[i].style.display = "none";
@ -237,12 +237,12 @@
function update(u) { function update(u) {
const { left, top } = u.cursor; const { left, top } = u.cursor;
const width = u.over.querySelector(".u-legend").offsetWidth; const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
legendEl.style.transform = legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)"; "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
} }
if (dataSize <= 12 || useStatsSeries === true) { if (dataSize <= 12 || useStatsSeries) {
return { return {
hooks: { hooks: {
init: init, init: init,
@ -311,13 +311,6 @@
} }
} }
const plotSeries = [
{
label: "Runtime",
value: (u, ts, sidx, didx) =>
didx == null ? null : formatTime(ts, forNode),
},
];
const plotData = [new Array(longestSeries)]; const plotData = [new Array(longestSeries)];
if (forNode === true) { if (forNode === true) {
// Negative Timestamp Buildup // Negative Timestamp Buildup
@ -334,6 +327,15 @@
plotData[0][j] = j * timestep; plotData[0][j] = j * timestep;
} }
const plotSeries = [
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
{
label: "Runtime",
value: (u, ts, sidx, didx) =>
(didx == null) ? null : formatTime(ts, forNode),
}
];
let plotBands = undefined; let plotBands = undefined;
if (useStatsSeries) { if (useStatsSeries) {
plotData.push(statisticsSeries.min); plotData.push(statisticsSeries.min);
@ -370,16 +372,61 @@
} else { } else {
for (let i = 0; i < series.length; i++) { for (let i = 0; i < series.length; i++) {
plotData.push(series[i].data); plotData.push(series[i].data);
// Default
if (!extendedLegendData) {
plotSeries.push({ plotSeries.push({
label: label:
scope === "node" scope === "node"
? series[i].hostname ? series[i].hostname
: scope === "accelerator"
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
: scope + " #" + (i + 1), : scope + " #" + (i + 1),
scale: "y", scale: "y",
width: lineWidth, width: lineWidth,
stroke: lineColor(i, series.length), stroke: lineColor(i, series.length),
}); });
} }
// Extended Legend For NodeList
else {
plotSeries.push({
label:
scope === "node"
? series[i].hostname
: scope === "accelerator"
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
: scope + " #" + (i + 1),
scale: "y",
width: lineWidth,
stroke: lineColor(i, series.length),
values: (u, sidx, idx) => {
// "i" = "sidx - 1" : sidx contains x-axis-data
if (idx == null)
return {
time: '-',
value: '-',
user: '-',
job: '-'
};
if (series[i].id in extendedLegendData) {
return {
time: formatTime(plotData[0][idx], forNode),
value: plotData[sidx][idx],
user: extendedLegendData[series[i].id].user,
job: extendedLegendData[series[i].id].job,
};
} else {
return {
time: formatTime(plotData[0][idx], forNode),
value: plotData[sidx][idx],
user: '-',
job: '-',
};
}
}
});
}
}
} }
const opts = { const opts = {
@ -434,13 +481,13 @@
u.ctx.save(); u.ctx.save();
u.ctx.textAlign = "start"; // 'end' u.ctx.textAlign = "start"; // 'end'
u.ctx.fillStyle = "black"; u.ctx.fillStyle = "black";
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10); u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
u.ctx.textAlign = "end"; u.ctx.textAlign = "end";
u.ctx.fillStyle = "black"; u.ctx.fillStyle = "black";
u.ctx.fillText( u.ctx.fillText(
textr, textr,
u.bbox.left + u.bbox.width - 10, u.bbox.left + u.bbox.width - 10,
u.bbox.top + 10, u.bbox.top + (forNode ? 0 : 10),
); );
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right // u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
@ -498,10 +545,12 @@
}, },
legend: { legend: {
// Display legend until max 12 Y-dataseries // Display legend until max 12 Y-dataseries
show: series.length <= 12 || useStatsSeries === true ? true : false, show: series.length <= 12 || useStatsSeries,
live: series.length <= 12 || useStatsSeries === true ? true : false, live: series.length <= 12 || useStatsSeries,
}, },
cursor: { drag: { x: true, y: true } }, cursor: {
drag: { x: true, y: true },
}
}; };
// RENDER HANDLING // RENDER HANDLING
@ -537,17 +586,9 @@
} }
onMount(() => { onMount(() => {
// Setup Wrapper if (plotWrapper) {
if (series[0].data.length > 0) {
if (forNode) {
plotWrapper.style.paddingTop = "0.5rem"
plotWrapper.style.paddingBottom = "0.5rem"
}
plotWrapper.style.backgroundColor = backgroundColor();
plotWrapper.style.borderRadius = "5px";
}
// Init Plot
render(width, height); render(width, height);
}
}); });
onDestroy(() => { onDestroy(() => {
@ -555,22 +596,20 @@
if (uplot) uplot.destroy(); if (uplot) uplot.destroy();
}); });
// This updates it on all size changes // This updates plot on all size changes if wrapper (== data) exists
// Condition for reactive triggering (eg scope change) $: if (plotWrapper) {
$: if (series[0].data.length > 0) {
onSizeChange(width, height); onSizeChange(width, height);
} }
</script> </script>
<!-- Define Wrapper and NoData Card within $width --> <!-- Define $width Wrapper and NoData Card -->
<div bind:clientWidth={width}> {#if series[0]?.data && series[0].data.length > 0}
{#if series[0].data.length > 0} <div bind:this={plotWrapper} bind:clientWidth={width}
<div bind:this={plotWrapper}/> style="background-color: {backgroundColor()};" class={forNode ? 'py-2 rounded' : 'rounded'}
/>
{:else} {:else}
<Card class="mx-4" body color="warning" <Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{metric}</code></Card >Cannot render plot: No series data returned for <code>{metric}</code></Card
> >
{/if} {/if}
</div>

View File

@ -12,7 +12,7 @@
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext, createEventDispatcher } from "svelte";
import { import {
Modal, Modal,
ModalBody, ModalBody,
@ -33,6 +33,7 @@
const onInit = getContext("on-init") const onInit = getContext("on-init")
const globalMetrics = getContext("globalMetrics") const globalMetrics = getContext("globalMetrics")
const dispatch = createEventDispatcher();
let newMetricsOrder = []; let newMetricsOrder = [];
let unorderedMetrics = [...metrics]; let unorderedMetrics = [...metrics];
@ -128,6 +129,8 @@
throw res.error; throw res.error;
} }
}); });
dispatch('update-metrics', metrics);
} }
</script> </script>
@ -175,6 +178,7 @@
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button> <Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Cancel</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -304,8 +304,19 @@ export function stickyHeader(datatableHeaderSelector, updatePading) {
export function checkMetricDisabled(m, c, s) { // [m]etric, [c]luster, [s]ubcluster export function checkMetricDisabled(m, c, s) { // [m]etric, [c]luster, [s]ubcluster
const metrics = getContext("globalMetrics"); const metrics = getContext("globalMetrics");
const result = metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s) const available = metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s)
return !result // Return inverse logic
return !available
}
export function checkMetricsDisabled(ma, c, s) { // [m]etric[a]rray, [c]luster, [s]ubcluster
let result = {};
const metrics = getContext("globalMetrics");
ma.forEach((m) => {
// Return named inverse logic: !available
result[m] = !(metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s))
});
return result
} }
export function getStatsItems(presetStats = []) { export function getStatsItems(presetStats = []) {

View File

@ -3,6 +3,7 @@
Properties: Properties:
- `clusters [String]`: List of cluster names - `clusters [String]`: List of cluster names
- `subClusters map[String][]string`: Map of subclusters by cluster names
- `links [Object]`: Pre-filtered link objects based on user auth - `links [Object]`: Pre-filtered link objects based on user auth
- `direction String?`: The direcion of the drop-down menue [default: down] - `direction String?`: The direcion of the drop-down menue [default: down]
--> -->
@ -18,12 +19,49 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let clusters; export let clusters;
export let subClusters;
export let links; export let links;
export let direction = "down"; export let direction = "down";
</script> </script>
{#each links as item} {#each links as item}
{#if item.listOptions} {#if item.listOptions}
{#if item.title === 'Nodes'}
<Dropdown nav inNavbar {direction}>
<DropdownToggle nav caret>
<Icon name={item.icon} />
{item.title}
</DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end">
{#each clusters as cluster}
<Dropdown nav direction="right">
<DropdownToggle nav caret class="dropdown-item py-1 px-2">
{cluster.name}
</DropdownToggle>
<DropdownMenu>
<DropdownItem class="py-1 px-2"
href={item.href + cluster.name}
>
Node Overview
</DropdownItem>
<DropdownItem class="py-1 px-2"
href={item.href + 'list/' + cluster.name}
>
Node List
</DropdownItem>
{#each subClusters[cluster.name] as subCluster}
<DropdownItem class="py-1 px-2"
href={item.href + 'list/' + cluster.name + '/' + subCluster}
>
{subCluster} Node List
</DropdownItem>
{/each}
</DropdownMenu>
</Dropdown>
{/each}
</DropdownMenu>
</Dropdown>
{:else}
<Dropdown nav inNavbar {direction}> <Dropdown nav inNavbar {direction}>
<DropdownToggle nav caret> <DropdownToggle nav caret>
<Icon name={item.icon} /> <Icon name={item.icon} />
@ -57,6 +95,7 @@
{/each} {/each}
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
{/if}
{:else if !item.perCluster} {:else if !item.perCluster}
<NavLink href={item.href} active={window.location.pathname == item.href} <NavLink href={item.href} active={window.location.pathname == item.href}
><Icon name={item.icon} /> {item.title}</NavLink ><Icon name={item.icon} /> {item.title}</NavLink

View File

@ -4,11 +4,14 @@ import Systems from './Systems.root.svelte'
new Systems({ new Systems({
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
displayType: displayType,
cluster: infos.cluster, cluster: infos.cluster,
subCluster: infos.subCluster,
from: infos.from, from: infos.from,
to: infos.to to: infos.to
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig] ['cc-config', clusterCockpitConfig],
['resampling', resampleConfig]
]) ])
}) })

View File

@ -0,0 +1,280 @@
<!--
@component Cluster Per Node List component; renders current state of SELECTABLE metrics for ALL nodes
Properties:
- `cluster String`: The nodes' cluster
- `subCluster String`: The nodes' subCluster
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
- `selectedMetrics [String]`: The array of selected metrics
- `systemUnits Object`: The object of metric units
-->
<script>
import { getContext } from "svelte";
import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte";
import { Row, Col, Card, Table, Spinner } from "@sveltestrap/sveltestrap";
import { stickyHeader } from "../generic/utils.js";
import NodeListRow from "./nodelist/NodeListRow.svelte";
import Pagination from "../generic/joblist/Pagination.svelte";
export let cluster;
export let subCluster = "";
export let ccconfig = null;
export let selectedMetrics = [];
export let selectedResolution = 0;
export let hostnameFilter = "";
export let systemUnits = null;
export let from = null;
export let to = null;
// Decouple from Job List Paging Params?
let usePaging = ccconfig?.node_list_usePaging || false
let itemsPerPage = usePaging ? (ccconfig?.plot_list_nodesPerPage || 10) : 10;
let page = 1;
let paging = { itemsPerPage, page };
let headerPaddingTop = 0;
stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x),
);
// const { query: initq } = init();
const initialized = getContext("initialized");
const client = getContextClient();
const nodeListQuery = gql`
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
nodeMetricsList(
cluster: $cluster
subCluster: $subCluster
nodeFilter: $nodeFilter
scopes: $scopes
metrics: $metrics
from: $from
to: $to
page: $paging
resolution: $selectedResolution
) {
items {
host
subCluster
metrics {
name
scope
metric {
timestep
unit {
base
prefix
}
series {
id
hostname
data
statistics {
min
avg
max
}
}
statisticsSeries {
min
median
max
}
}
}
}
totalNodes
hasNextPage
}
}
`
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
// Decouple from Job List Paging Params?
function updateConfiguration(value, page) {
updateConfigurationMutation({
name: "plot_list_nodesPerPage",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
nodes = [] // Empty List
paging = { itemsPerPage: value, page: page }; // Trigger reload of nodeList
} else if (res.fetching === false && res.error) {
throw res.error;
}
});
}
if (!usePaging) {
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $nodesQuery?.data != null && $nodesQuery.data?.nodeMetricsList.hasNextPage) {
let pendingPaging = { ...paging }
pendingPaging.page += 1
paging = pendingPaging
};
});
};
$: nodesQuery = queryStore({
client: client,
query: nodeListQuery,
variables: {
cluster: cluster,
subCluster: subCluster,
nodeFilter: hostnameFilter,
scopes: ["core", "socket", "accelerator"],
metrics: selectedMetrics,
from: from.toISOString(),
to: to.toISOString(),
paging: paging,
selectedResolution: selectedResolution,
},
requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change
});
let nodes = [];
$: if ($initialized && $nodesQuery.data) {
if (usePaging) {
nodes = [...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host));
} else {
nodes = nodes.concat([...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host)))
}
}
$: if (!usePaging && (selectedMetrics || selectedResolution || hostnameFilter || from || to)) {
// Continous Scroll: Reset list and paging if parameters change: Existing entries will not match new selections
nodes = [];
paging = { itemsPerPage, page: 1 };
}
$: matchedNodes = $nodesQuery.data?.nodeMetricsList.totalNodes || matchedNodes;
</script>
<Row>
<div class="col cc-table-wrapper">
<Table cellspacing="0px" cellpadding="0px">
<thead>
<tr>
<th
class="position-sticky top-0 text-capitalize"
scope="col"
style="padding-top: {headerPaddingTop}px;"
>
{cluster} Node Info
{#if $nodesQuery.fetching}
<Spinner size="sm" style="margin-left:10px;" secondary />
{/if}
</th>
{#each selectedMetrics as metric (metric)}
<th
class="position-sticky top-0 text-center"
scope="col"
style="padding-top: {headerPaddingTop}px"
>
{metric} ({systemUnits[metric]})
</th>
{/each}
</tr>
</thead>
<tbody>
{#if $nodesQuery.error}
<Row>
<Col>
<Card body color="danger">{$nodesQuery.error.message}</Card>
</Col>
</Row>
{:else}
{#each nodes as nodeData}
<NodeListRow {nodeData} {cluster} {selectedMetrics}/>
{:else}
<tr>
<td colspan={selectedMetrics.length + 1}> No nodes found </td>
</tr>
{/each}
{/if}
{#if $nodesQuery.fetching || !$nodesQuery.data}
<tr>
<td colspan={selectedMetrics.length + 1}>
<div style="text-align:center;">
<p><b>
Loading nodes {nodes.length + 1} to
{ matchedNodes
? `${(nodes.length + paging.itemsPerPage) > matchedNodes ? matchedNodes : (nodes.length + paging.itemsPerPage)} of ${matchedNodes} total`
: (nodes.length + paging.itemsPerPage)
}
</b></p>
<Spinner secondary />
</div>
</td>
</tr>
{/if}
</tbody>
</Table>
</div>
</Row>
{#if usePaging}
<Pagination
bind:page
{itemsPerPage}
itemText="Nodes"
totalItems={matchedNodes}
on:update-paging={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
} else {
nodes = []
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
}
}}
/>
{/if}
<style>
.cc-table-wrapper {
overflow: initial;
}
.cc-table-wrapper > :global(table) {
border-collapse: separate;
border-spacing: 0px;
table-layout: fixed;
}
.cc-table-wrapper :global(button) {
margin-bottom: 0px;
}
.cc-table-wrapper > :global(table > tbody > tr > td) {
margin: 0px;
padding-left: 5px;
padding-right: 0px;
}
th.position-sticky.top-0 {
background-color: white;
z-index: 10;
border-bottom: 1px solid black;
}
</style>

View File

@ -0,0 +1,155 @@
<!--
@component Cluster Per Node Overview component; renders current state of ONE metric for ALL nodes
Properties:
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
- `cluster String`: The cluster to show status information for
- `selectedMetric String?`: The selectedMetric input [Default: ""]
-->
<script>
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { Row, Col, Card, Spinner } from "@sveltestrap/sveltestrap";
import { init, checkMetricsDisabled } from "../generic/utils.js";
import MetricPlot from "../generic/plots/MetricPlot.svelte";
export let ccconfig = null;
export let cluster = "";
export const subCluster = "";
export let selectedMetrics = null;
export let hostnameFilter = "";
export let from = null;
export let to = null;
const { query: initq } = init();
const client = getContextClient();
const nodeQuery = gql`
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(
cluster: $cluster
metrics: $metrics
from: $from
to: $to
) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit {
base
prefix
}
series {
statistics {
min
avg
max
}
data
}
}
}
}
}
`
$: selectedMetric = selectedMetrics[0] ? selectedMetrics[0] : "";
$: nodesQuery = queryStore({
client: client,
query: nodeQuery,
variables: {
cluster: cluster,
metrics: selectedMetrics,
from: from.toISOString(),
to: to.toISOString(),
},
});
let rawData = []
$: if ($initq.data && $nodesQuery?.data) {
rawData = $nodesQuery?.data?.nodeMetrics.filter((h) => {
if (h.subCluster === '') { // Exclude nodes with empty subCluster field
console.warn('subCluster not configured for node', h.host)
return false
} else {
return h.metrics.some(
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
)
}
})
}
let mappedData = []
$: if (rawData?.length > 0) {
mappedData = rawData.map((h) => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.filter(
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
),
disabled: checkMetricsDisabled(
selectedMetrics,
cluster,
h.subCluster,
),
}))
.sort((a, b) => a.host.localeCompare(b.host))
}
let filteredData = []
$: if (mappedData?.length > 0) {
filteredData = mappedData.filter((h) =>
h.host.includes(hostnameFilter)
)
}
</script>
{#if $nodesQuery.error}
<Row>
<Col>
<Card body color="danger">{$nodesQuery.error.message}</Card>
</Col>
</Row>
{:else if $nodesQuery.fetching }
<Row>
<Col>
<Spinner />
</Col>
</Row>
{:else if filteredData?.length > 0}
<!-- PlotGrid flattened into this component -->
<Row cols={{ xs: 1, sm: 2, md: 3, lg: ccconfig.plot_view_plotsPerRow}}>
{#each filteredData as item (item.host)}
<Col class="px-1">
<h4 style="width: 100%; text-align: center;">
<a
style="display: block;padding-top: 15px;"
href="/monitoring/node/{cluster}/{item.host}"
>{item.host} ({item.subCluster})</a
>
</h4>
{#if item?.disabled[selectedMetric]}
<Card body class="mx-3" color="info"
>Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code
></Card
>
{:else}
<!-- "No Data"-Warning included in MetricPlot-Component -->
<MetricPlot
timestep={item.data[0].metric.timestep}
series={item.data[0].metric.series}
metric={item.data[0].name}
{cluster}
subCluster={item.subCluster}
forNode
/>
{/if}
</Col>
{/each}
</Row>
{/if}

View File

@ -0,0 +1,177 @@
<!--
@component Displays node info, serves links to single node page and lists
Properties:
- `cluster String`: The nodes' cluster
- `subCluster String`: The nodes' subCluster
- `cluster String`: The nodes' hostname
-->
<script>
import {
Icon,
Button,
Card,
CardHeader,
CardBody,
Input,
InputGroup,
InputGroupText, } from "@sveltestrap/sveltestrap";
export let cluster;
export let subCluster
export let hostname;
export let dataHealth;
export let nodeJobsData = null;
// Not at least one returned, selected metric: NodeHealth warning
const healthWarn = !dataHealth.includes(true);
// At least one non-returned selected metric: Metric config error?
const metricWarn = dataHealth.includes(false);
let userList;
let projectList;
$: if (nodeJobsData) {
userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b));
projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b));
}
</script>
<Card class="pb-3">
<CardHeader class="d-inline-flex justify-content-between align-items-end">
<div>
<h5 class="mb-0">
Node
<a href="/monitoring/node/{cluster}/{hostname}" target="_blank">
{hostname}
</a>
</h5>
</div>
<div class="text-capitalize">
<h6 class="mb-0">
{cluster} {subCluster}
</h6>
</div>
</CardHeader>
<CardBody>
{#if healthWarn}
<InputGroup>
<InputGroupText>
<Icon name="exclamation-circle"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="danger" disabled>
Unhealthy
</Button>
</InputGroup>
{:else if metricWarn}
<InputGroup>
<InputGroupText>
<Icon name="info-circle"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="warning" disabled>
Missing Metric
</Button>
</InputGroup>
{:else if nodeJobsData.jobs.count == 1 && nodeJobsData.jobs.items[0].exclusive}
<InputGroup>
<InputGroupText>
<Icon name="circle-fill"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="success" disabled>
Exclusive
</Button>
</InputGroup>
{:else if nodeJobsData.jobs.count >= 1 && !nodeJobsData.jobs.items[0].exclusive}
<InputGroup>
<InputGroupText>
<Icon name="circle-half"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="success" disabled>
Shared
</Button>
</InputGroup>
{:else}
<InputGroup>
<InputGroupText>
<Icon name="circle"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="secondary" disabled>
Idle
</Button>
</InputGroup>
{/if}
<hr class="my-3"/>
<!-- JOBS -->
<InputGroup size="sm" class="justify-content-between mb-3">
<InputGroupText>
<Icon name="activity"/>
</InputGroupText>
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
Activity
</InputGroupText>
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{nodeJobsData?.jobs?.count || 0} Job{(nodeJobsData?.jobs?.count == 1) ? '': 's'}" disabled />
<a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
<Icon name="view-list" />
List
</a>
</InputGroup>
<!-- USERS -->
<InputGroup size="sm" class="justify-content-between {(userList?.length > 0) ? 'mb-1' : 'mb-3'}">
<InputGroupText>
<Icon name="people"/>
</InputGroupText>
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
Users
</InputGroupText>
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{userList?.length || 0} User{(userList?.length == 1) ? '': 's'}" disabled />
<a title="Show users active on this node" href="/monitoring/users/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
<Icon name="view-list" />
List
</a>
</InputGroup>
{#if userList?.length > 0}
<Card class="mb-3">
<div class="p-1">
{userList.join(", ")}
</div>
</Card>
{/if}
<!-- PROJECTS -->
<InputGroup size="sm" class="justify-content-between {(projectList?.length > 0) ? 'mb-1' : 'mb-3'}">
<InputGroupText>
<Icon name="journals"/>
</InputGroupText>
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
Projects
</InputGroupText>
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{projectList?.length || 0} Project{(projectList?.length == 1) ? '': 's'}" disabled />
<a title="Show projects active on this node" href="/monitoring/projects/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
<Icon name="view-list" />
List
</a>
</InputGroup>
{#if projectList?.length > 0}
<Card>
<div class="p-1">
{projectList.join(", ")}
</div>
</Card>
{/if}
</CardBody>
</Card>

View File

@ -0,0 +1,187 @@
<!--
@component Data row for a single node displaying metric plots
Properties:
- `cluster String`: The nodes' cluster
- `nodeData Object`: The node data object including metric data
- `selectedMetrics [String]`: The array of selected metrics
-->
<script>
import {
queryStore,
gql,
getContextClient,
} from "@urql/svelte";
import { Card, CardBody, Spinner } from "@sveltestrap/sveltestrap";
import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
import MetricPlot from "../../generic/plots/MetricPlot.svelte";
import NodeInfo from "./NodeInfo.svelte";
export let cluster;
export let nodeData;
export let selectedMetrics;
const client = getContextClient();
const paging = { itemsPerPage: 50, page: 1 };
const sorting = { field: "startTime", type: "col", order: "DESC" };
const filter = [
{ cluster: { eq: cluster } },
{ node: { contains: nodeData.host } },
{ state: ["running"] },
];
const nodeJobsQuery = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
items {
jobId
user
project
exclusive
resources {
hostname
accelerators
}
}
count
}
}
`;
$: nodeJobsData = queryStore({
client: client,
query: nodeJobsQuery,
variables: { paging, sorting, filter },
});
// Helper
const selectScope = (nodeMetrics) =>
nodeMetrics.reduce(
(a, b) =>
maxScope([a.scope, b.scope]) == a.scope ? b : a,
nodeMetrics[0],
);
const sortAndSelectScope = (allNodeMetrics) =>
selectedMetrics
.map((selectedName) => allNodeMetrics.filter((nodeMetric) => nodeMetric.name == selectedName))
.map((matchedNodeMetrics) => ({
disabled: false,
data: matchedNodeMetrics.length > 0 ? selectScope(matchedNodeMetrics) : null,
}))
.map((scopedNodeMetric) => {
if (scopedNodeMetric?.data) {
return {
disabled: checkMetricDisabled(
scopedNodeMetric.data.name,
cluster,
nodeData.subCluster,
),
data: scopedNodeMetric.data,
};
} else {
return scopedNodeMetric;
}
});
let refinedData;
let dataHealth;
$: if (nodeData?.metrics) {
refinedData = sortAndSelectScope(nodeData?.metrics)
dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0))
}
let extendedLegendData = null;
$: if ($nodeJobsData?.data) {
// Get Shared State of Node: Only Build extended Legend For Shared Nodes
if ($nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive) {
const accSet = Array.from(new Set($nodeJobsData.data.jobs.items
.map((i) => i.resources
.filter((r) => r.hostname === nodeData.host)
.map((r) => r.accelerators)
)
)).flat(2)
extendedLegendData = {}
for (const accId of accSet) {
const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId)))
extendedLegendData[accId] = {
user: matchJob?.user ? matchJob?.user : '-',
job: matchJob?.jobId ? matchJob?.jobId : '-',
}
}
// Theoretically extendable for hwthreadIDs
}
}
</script>
<tr>
<td>
{#if $nodeJobsData.fetching}
<Card>
<CardBody class="content-center">
<Spinner/>
</CardBody>
</Card>
{:else}
<NodeInfo nodeJobsData={$nodeJobsData.data} {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} {dataHealth}/>
{/if}
</td>
{#each refinedData as metricData (metricData.data.name)}
{#key metricData}
<td>
{#if metricData?.disabled}
<Card body class="mx-3" color="info"
>Metric disabled for subcluster <code
>{metricData.data.name}:{nodeData.subCluster}</code
></Card
>
{:else if !!metricData.data?.metric.statisticsSeries}
<!-- "No Data"-Warning included in MetricPlot-Component -->
<MetricPlot
{cluster}
subCluster={nodeData.subCluster}
metric={metricData.data.name}
scope={metricData.data.scope}
timestep={metricData.data.metric.timestep}
series={metricData.data.metric.series}
statisticsSeries={metricData.data?.metric.statisticsSeries}
useStatsSeries={!!metricData.data?.metric.statisticsSeries}
height={175}
forNode
/>
<div class="my-2"/>
{#key extendedLegendData}
<MetricPlot
{cluster}
subCluster={nodeData.subCluster}
metric={metricData.data.name}
scope={metricData.data.scope}
timestep={metricData.data.metric.timestep}
series={metricData.data.metric.series}
height={175}
{extendedLegendData}
forNode
/>
{/key}
{:else}
<MetricPlot
{cluster}
subCluster={nodeData.subCluster}
metric={metricData.data.name}
scope={metricData.data.scope}
timestep={metricData.data.metric.timestep}
series={metricData.data.metric.series}
height={375}
forNode
/>
{/if}
</td>
{/key}
{/each}
</tr>

View File

@ -18,6 +18,7 @@
"username": "{{ .User.Username }}", "username": "{{ .User.Username }}",
"authlevel": {{ .User.GetAuthLevel }}, "authlevel": {{ .User.GetAuthLevel }},
"clusters": {{ .Clusters }}, "clusters": {{ .Clusters }},
"subClusters": {{ .SubClusters }},
"roles": {{ .Roles }} "roles": {{ .Roles }}
}; };
</script> </script>

View File

@ -8,6 +8,7 @@
{{define "javascript"}} {{define "javascript"}}
<script> <script>
const isAdmin = {{ .User.HasRole .Roles.admin }}; const isAdmin = {{ .User.HasRole .Roles.admin }};
const isSupport = {{ .User.HasRole .Roles.support }};
const isApi = {{ .User.HasRole .Roles.api }}; const isApi = {{ .User.HasRole .Roles.api }};
const username = {{ .User.Username }}; const username = {{ .User.Username }};
const filterPresets = {{ .FilterPresets }}; const filterPresets = {{ .FilterPresets }};

View File

@ -7,8 +7,10 @@
{{end}} {{end}}
{{define "javascript"}} {{define "javascript"}}
<script> <script>
const displayType = {{ .Infos.displayType }};
const infos = {{ .Infos }}; const infos = {{ .Infos }};
const clusterCockpitConfig = {{ .Config }}; const clusterCockpitConfig = {{ .Config }};
const resampleConfig = {{ .Resampling }};
</script> </script>
<script src='/build/systems.js'></script> <script src='/build/systems.js'></script>
{{end}} {{end}}

View File

@ -13,6 +13,7 @@ import (
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/util" "github.com/ClusterCockpit/cc-backend/internal/util"
"github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
) )
@ -95,6 +96,7 @@ type Page struct {
Roles map[string]schema.Role // Available roles for frontend render checks Roles map[string]schema.Role // Available roles for frontend render checks
Build Build // Latest information about the application Build Build // Latest information about the application
Clusters []schema.ClusterConfig // List of all clusters for use in the Header Clusters []schema.ClusterConfig // List of all clusters for use in the Header
SubClusters map[string][]string // Map per cluster of all subClusters for use in the Header
FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters. FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
Infos map[string]interface{} // For generic use (e.g. username for /monitoring/user/<id>, job id for /monitoring/job/<id>) Infos map[string]interface{} // For generic use (e.g. username for /monitoring/user/<id>, job id for /monitoring/job/<id>)
Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...) Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
@ -114,6 +116,15 @@ func RenderTemplate(rw http.ResponseWriter, file string, page *Page) {
} }
} }
if page.SubClusters == nil {
page.SubClusters = make(map[string][]string)
for _, cluster := range archive.Clusters {
for _, sc := range cluster.SubClusters {
page.SubClusters[cluster.Name] = append(page.SubClusters[cluster.Name], sc.Name)
}
}
}
log.Debugf("Page config : %v\n", page.Config) log.Debugf("Page config : %v\n", page.Config)
if err := t.Execute(rw, page); err != nil { if err := t.Execute(rw, page); err != nil {
log.Errorf("Template error: %s", err.Error()) log.Errorf("Template error: %s", err.Error())