diff --git a/api/schema.graphqls b/api/schema.graphqls index cf8f5273..6330bcb5 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -143,6 +143,7 @@ type MetricConfig { alert: Float! lowerIsBetter: Boolean subClusters: [SubClusterConfig!]! + tooltip: String } type Tag { @@ -300,6 +301,7 @@ type GlobalMetricListItem { unit: Unit! scope: MetricScope! footprint: String + tooltip: String availability: [ClusterSupport!]! } diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index 22aaafac..613943f7 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -32,7 +32,9 @@ type Config = graphql.Config[ResolverRoot, DirectiveRoot, ComplexityRoot] type ResolverRoot interface { Cluster() ClusterResolver + GlobalMetricListItem() GlobalMetricListItemResolver Job() JobResolver + MetricConfig() MetricConfigResolver MetricValue() MetricValueResolver Mutation() MutationResolver Node() NodeResolver @@ -100,6 +102,7 @@ type ComplexityRoot struct { Footprint func(childComplexity int) int Name func(childComplexity int) int Scope func(childComplexity int) int + Tooltip func(childComplexity int) int Unit func(childComplexity int) int } @@ -219,6 +222,7 @@ type ComplexityRoot struct { Scope func(childComplexity int) int SubClusters func(childComplexity int) int Timestep func(childComplexity int) int + Tooltip func(childComplexity int) int Unit func(childComplexity int) int } @@ -441,6 +445,9 @@ type ComplexityRoot struct { type ClusterResolver interface { Partitions(ctx context.Context, obj *schema.Cluster) ([]string, error) } +type GlobalMetricListItemResolver interface { + Tooltip(ctx context.Context, obj *schema.GlobalMetricListItem) (*string, error) +} type JobResolver interface { StartTime(ctx context.Context, obj *schema.Job) (*time.Time, error) @@ -452,6 +459,9 @@ type JobResolver interface { MetaData(ctx context.Context, obj *schema.Job) (any, error) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) } +type MetricConfigResolver interface { + Tooltip(ctx context.Context, obj *schema.MetricConfig) (*string, error) +} type MetricValueResolver interface { Name(ctx context.Context, obj *schema.MetricValue) (*string, error) } @@ -690,6 +700,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.GlobalMetricListItem.Scope(childComplexity), true + case "GlobalMetricListItem.tooltip": + if e.ComplexityRoot.GlobalMetricListItem.Tooltip == nil { + break + } + + return e.ComplexityRoot.GlobalMetricListItem.Tooltip(childComplexity), true case "GlobalMetricListItem.unit": if e.ComplexityRoot.GlobalMetricListItem.Unit == nil { break @@ -1217,6 +1233,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.ComplexityRoot.MetricConfig.Timestep(childComplexity), true + case "MetricConfig.tooltip": + if e.ComplexityRoot.MetricConfig.Tooltip == nil { + break + } + + return e.ComplexityRoot.MetricConfig.Tooltip(childComplexity), true case "MetricConfig.unit": if e.ComplexityRoot.MetricConfig.Unit == nil { break @@ -2418,6 +2440,7 @@ type MetricConfig { alert: Float! lowerIsBetter: Boolean subClusters: [SubClusterConfig!]! + tooltip: String } type Tag { @@ -2575,6 +2598,7 @@ type GlobalMetricListItem { unit: Unit! scope: MetricScope! footprint: String + tooltip: String availability: [ClusterSupport!]! } @@ -2957,6 +2981,8 @@ func (ec *executionContext) childFields_GlobalMetricListItem(ctx context.Context return ec.fieldContext_GlobalMetricListItem_scope(ctx, field) case "footprint": return ec.fieldContext_GlobalMetricListItem_footprint(ctx, field) + case "tooltip": + return ec.fieldContext_GlobalMetricListItem_tooltip(ctx, field) case "availability": return ec.fieldContext_GlobalMetricListItem_availability(ctx, field) } @@ -3187,6 +3213,8 @@ func (ec *executionContext) childFields_MetricConfig(ctx context.Context, field return ec.fieldContext_MetricConfig_lowerIsBetter(ctx, field) case "subClusters": return ec.fieldContext_MetricConfig_subClusters(ctx, field) + case "tooltip": + return ec.fieldContext_MetricConfig_tooltip(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type MetricConfig", field.Name) } @@ -5159,6 +5187,29 @@ func (ec *executionContext) fieldContext_GlobalMetricListItem_footprint(_ contex return graphql.NewScalarFieldContext("GlobalMetricListItem", field, false, false, errors.New("field of type String does not have child fields")) } +func (ec *executionContext) _GlobalMetricListItem_tooltip(ctx context.Context, field graphql.CollectedField, obj *schema.GlobalMetricListItem) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_GlobalMetricListItem_tooltip(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.GlobalMetricListItem().Tooltip(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2áš–string(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_GlobalMetricListItem_tooltip(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("GlobalMetricListItem", field, true, true, errors.New("field of type String does not have child fields")) +} + func (ec *executionContext) _GlobalMetricListItem_availability(ctx context.Context, field graphql.CollectedField, obj *schema.GlobalMetricListItem) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7349,6 +7400,29 @@ func (ec *executionContext) fieldContext_MetricConfig_subClusters(_ context.Cont return fc, nil } +func (ec *executionContext) _MetricConfig_tooltip(ctx context.Context, field graphql.CollectedField, obj *schema.MetricConfig) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_MetricConfig_tooltip(ctx, field) + }, + func(ctx context.Context) (any, error) { + return ec.Resolvers.MetricConfig().Tooltip(ctx, obj) + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2áš–string(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_MetricConfig_tooltip(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("MetricConfig", field, true, true, errors.New("field of type String does not have child fields")) +} + func (ec *executionContext) _MetricFootprints_metric(ctx context.Context, field graphql.CollectedField, obj *model.MetricFootprints) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -13269,24 +13343,57 @@ func (ec *executionContext) _GlobalMetricListItem(ctx context.Context, sel ast.S case "name": out.Values[i] = ec._GlobalMetricListItem_name(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "unit": out.Values[i] = ec._GlobalMetricListItem_unit(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "scope": out.Values[i] = ec._GlobalMetricListItem_scope(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "footprint": out.Values[i] = ec._GlobalMetricListItem_footprint(ctx, field, obj) + case "tooltip": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._GlobalMetricListItem_tooltip(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "availability": out.Values[i] = ec._GlobalMetricListItem_availability(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } default: panic("unknown field " + strconv.Quote(field.Name)) @@ -14209,52 +14316,85 @@ func (ec *executionContext) _MetricConfig(ctx context.Context, sel ast.Selection case "name": out.Values[i] = ec._MetricConfig_name(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "unit": out.Values[i] = ec._MetricConfig_unit(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "scope": out.Values[i] = ec._MetricConfig_scope(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "aggregation": out.Values[i] = ec._MetricConfig_aggregation(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "timestep": out.Values[i] = ec._MetricConfig_timestep(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "peak": out.Values[i] = ec._MetricConfig_peak(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "normal": out.Values[i] = ec._MetricConfig_normal(ctx, field, obj) case "caution": out.Values[i] = ec._MetricConfig_caution(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "alert": out.Values[i] = ec._MetricConfig_alert(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "lowerIsBetter": out.Values[i] = ec._MetricConfig_lowerIsBetter(ctx, field, obj) case "subClusters": out.Values[i] = ec._MetricConfig_subClusters(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } + case "tooltip": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._MetricConfig_tooltip(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 3ef58ad4..21c6c251 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -32,6 +32,11 @@ func (r *clusterResolver) Partitions(ctx context.Context, obj *schema.Cluster) ( return r.Repo.Partitions(obj.Name) } +// Tooltip is the resolver for the tooltip field. +func (r *globalMetricListItemResolver) Tooltip(ctx context.Context, obj *schema.GlobalMetricListItem) (*string, error) { + return &obj.Tooltip, nil +} + // StartTime is the resolver for the startTime field. func (r *jobResolver) StartTime(ctx context.Context, obj *schema.Job) (*time.Time, error) { timestamp := time.Unix(obj.StartTime, 0) @@ -127,6 +132,11 @@ func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.Use return repository.GetUserRepository().FetchUserInCtx(ctx, obj.User) } +// Tooltip is the resolver for the tooltip field. +func (r *metricConfigResolver) Tooltip(ctx context.Context, obj *schema.MetricConfig) (*string, error) { + return &obj.Tooltip, nil +} + // Name is the resolver for the name field. func (r *metricValueResolver) Name(ctx context.Context, obj *schema.MetricValue) (*string, error) { panic(fmt.Errorf("not implemented: Name - name")) @@ -1054,9 +1064,17 @@ func (r *subClusterResolver) NumberOfNodes(ctx context.Context, obj *schema.SubC // Cluster returns generated.ClusterResolver implementation. func (r *Resolver) Cluster() generated.ClusterResolver { return &clusterResolver{r} } +// GlobalMetricListItem returns generated.GlobalMetricListItemResolver implementation. +func (r *Resolver) GlobalMetricListItem() generated.GlobalMetricListItemResolver { + return &globalMetricListItemResolver{r} +} + // Job returns generated.JobResolver implementation. func (r *Resolver) Job() generated.JobResolver { return &jobResolver{r} } +// MetricConfig returns generated.MetricConfigResolver implementation. +func (r *Resolver) MetricConfig() generated.MetricConfigResolver { return &metricConfigResolver{r} } + // MetricValue returns generated.MetricValueResolver implementation. func (r *Resolver) MetricValue() generated.MetricValueResolver { return &metricValueResolver{r} } @@ -1073,7 +1091,9 @@ func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } func (r *Resolver) SubCluster() generated.SubClusterResolver { return &subClusterResolver{r} } type clusterResolver struct{ *Resolver } +type globalMetricListItemResolver struct{ *Resolver } type jobResolver struct{ *Resolver } +type metricConfigResolver struct{ *Resolver } type metricValueResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type nodeResolver struct{ *Resolver } diff --git a/pkg/archive/clusterConfig.go b/pkg/archive/clusterConfig.go index 48fc5e48..9e10c6e3 100644 --- a/pkg/archive/clusterConfig.go +++ b/pkg/archive/clusterConfig.go @@ -63,8 +63,11 @@ func initClusterConfig() error { if _, ok := metricLookup[mc.Name]; !ok { metricLookup[mc.Name] = schema.GlobalMetricListItem{ - Name: mc.Name, Scope: mc.Scope, Unit: mc.Unit, Footprint: mc.Footprint, + Name: mc.Name, Scope: mc.Scope, Unit: mc.Unit, Footprint: mc.Footprint, Tooltip: mc.Tooltip, } + } else if item := metricLookup[mc.Name]; item.Tooltip == "" && mc.Tooltip != "" { + item.Tooltip = mc.Tooltip + metricLookup[mc.Name] = item } availability := schema.ClusterSupport{Cluster: cluster.Name} @@ -139,8 +142,10 @@ func initClusterConfig() error { userItem, ok := userMetricLookup[mc.Name] if !ok { userItem = schema.GlobalMetricListItem{ - Name: mc.Name, Scope: mc.Scope, Unit: mc.Unit, Footprint: mc.Footprint, + Name: mc.Name, Scope: mc.Scope, Unit: mc.Unit, Footprint: mc.Footprint, Tooltip: mc.Tooltip, } + } else if userItem.Tooltip == "" && mc.Tooltip != "" { + userItem.Tooltip = mc.Tooltip } userItem.Availability = append(userItem.Availability, userAvailability) userMetricLookup[mc.Name] = userItem diff --git a/web/frontend/src/Jobs.root.svelte b/web/frontend/src/Jobs.root.svelte index 370f267c..17f87573 100644 --- a/web/frontend/src/Jobs.root.svelte +++ b/web/frontend/src/Jobs.root.svelte @@ -60,6 +60,8 @@ let presetProject = $derived(filterPresets?.project ? filterPresets.project : ""); let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null); let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null); + const maxClusters = $derived($initq?.data?.clusters?.length || 0); + const maxSubClusters = $derived($initq?.data?.clusters?.find((c) => c.name == selectedCluster)?.subClusters?.length || 0); let metrics = $derived.by(() => { if (thisInit && ccconfig) { if (selectedCluster) { @@ -243,6 +245,8 @@ presetMetrics={metrics} cluster={selectedCluster} subCluster={selectedSubCluster} + {maxClusters} + {maxSubClusters} configName="metricConfig_jobListMetrics" footprintSelect {globalMetrics} diff --git a/web/frontend/src/Systems.root.svelte b/web/frontend/src/Systems.root.svelte index 4cb85079..25620d1a 100644 --- a/web/frontend/src/Systems.root.svelte +++ b/web/frontend/src/Systems.root.svelte @@ -63,8 +63,9 @@ let pendingHostnameFilter = $state(""); let isMetricsSelectionOpen = $state(false); - /* Derived Init Return */ + /* Derived Init Returns */ const thisInit = $derived($initq?.data ? true : false); + const maxSubClusters = $derived($initq?.data?.clusters?.find((c) => c.name == cluster)?.subClusters?.length || 0); /* Derived States */ const ccconfig = $derived(thisInit ? getContext("cc-config") : null); @@ -284,6 +285,7 @@ {cluster} {subCluster} {globalMetrics} + maxSubClusters={subCluster? null: maxSubClusters} configName="nodeList_selectedMetrics" applyMetrics={(newMetrics) => selectedMetrics = [...newMetrics] diff --git a/web/frontend/src/User.root.svelte b/web/frontend/src/User.root.svelte index e1bc5483..98bd9ba9 100644 --- a/web/frontend/src/User.root.svelte +++ b/web/frontend/src/User.root.svelte @@ -90,6 +90,8 @@ const shortDuration = $derived(ccconfig?.jobList_hideShortRunningJobs); let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null); let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null); + const maxClusters = $derived($initq?.data?.clusters?.length || 0); + const maxSubClusters = $derived($initq?.data?.clusters?.find((c) => c.name == selectedCluster)?.subClusters?.length || 0); let metrics = $derived.by(() => { if (thisInit && ccconfig) { if (selectedCluster) { @@ -531,6 +533,8 @@ presetMetrics={metrics} cluster={selectedCluster} subCluster={selectedSubCluster} + {maxClusters} + {maxSubClusters} configName="metricConfig_jobListMetrics" footprintSelect {globalMetrics} diff --git a/web/frontend/src/generic/select/MetricSelection.svelte b/web/frontend/src/generic/select/MetricSelection.svelte index 8234b32c..9a15a854 100644 --- a/web/frontend/src/generic/select/MetricSelection.svelte +++ b/web/frontend/src/generic/select/MetricSelection.svelte @@ -22,6 +22,8 @@ ModalFooter, Button, ListGroup, + Icon, + Tooltip } from "@sveltestrap/sveltestrap"; import { gql, getContextClient, mutationStore } from "@urql/svelte"; @@ -33,6 +35,8 @@ presetMetrics = [], cluster = null, subCluster = null, + maxClusters = null, + maxSubClusters = null, footprintSelect = false, configName, globalMetrics, @@ -86,20 +90,47 @@ return availableMetrics; } + function printAvailabilityCount(metric, cluster) { + const avail = globalMetrics.find((gm) => gm.name === metric)?.availability + if (avail) { + if (!cluster) { + return `${avail.length} / ${maxClusters} Cluster` + } else { + const subAvail = avail.find((av) => av.cluster === cluster)?.subClusters + if (subAvail) { + return `${subAvail.length} / ${maxSubClusters} SubCluster` + } else { + return `0 / ${maxSubClusters} SubCluster` + } + } + } + return `0 / ${maxClusters} Cluster` + } + function printAvailability(metric, cluster) { const avail = globalMetrics.find((gm) => gm.name === metric)?.availability if (avail) { if (!cluster) { - return avail.map((av) => av.cluster).join(', ') + console.log(metric, avail.map((av) => av.cluster)) + return avail.map((av) => av.cluster) } else { const subAvail = avail.find((av) => av.cluster === cluster)?.subClusters if (subAvail) { - return subAvail.join(', ') + console.log(metric, subAvail) + return subAvail } else { - return `Not available for ${cluster}` + return [`Not available for ${cluster}`] } } } + return [`Not available for ${cluster}`] + } + + function printTooltip(metric) { + const toolt = globalMetrics.find((gm) => gm.name === metric)?.tooltip + if (toolt) { + return toolt + } return "" } @@ -172,7 +203,7 @@ (isOpen = !isOpen)}> - Configure columns (Metric availability shown) + Configure columns {#if footprintSelect} @@ -213,9 +244,34 @@ /> {/if} {metric} - - {printAvailability(metric, cluster)} - + {#if maxClusters !== null || maxSubClusters !== null} + + + + Availability +
    + {#each printAvailability(metric, cluster) as avail} +
  • {avail}
  • + {/each} +
+
+
+ {/if} + {#if printTooltip(metric) !== ""} + + + + Information +

+ { printTooltip(metric) } +

+
+
+ {/if} {/each}
diff --git a/web/frontend/src/generic/utils.js b/web/frontend/src/generic/utils.js index 6819bf56..f4b60034 100644 --- a/web/frontend/src/generic/utils.js +++ b/web/frontend/src/generic/utils.js @@ -81,6 +81,7 @@ export function init(extraInitQuery = "") { name scope footprint + tooltip unit { base, prefix } availability { cluster, subClusters } }