diff --git a/api/schema.graphqls b/api/schema.graphqls index 82c9488..70b9268 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -77,7 +77,7 @@ type SubClusterConfig { type MetricConfig { name: String! - unit: String! + unit: Unit scope: MetricScope! aggregation: String timestep: Int! @@ -107,7 +107,7 @@ type JobMetricWithName { } type JobMetric { - unit: String! + unit: Unit scope: MetricScope! timestep: Int! series: [Series!] @@ -121,6 +121,11 @@ type Series { data: [NullableFloat!]! } +type Unit { + base: String! + prefix: String +} + type MetricStatistics { avg: Float! min: Float! diff --git a/cmd/cc-backend/main.go b/cmd/cc-backend/main.go index c7c4b8e..9c7ce34 100644 --- a/cmd/cc-backend/main.go +++ b/cmd/cc-backend/main.go @@ -47,12 +47,12 @@ import ( ) const logoString = ` - ____ _ _ ____ _ _ _ -/ ___| |_ _ ___| |_ ___ _ __ / ___|___ ___| | ___ __ (_) |_ + ____ _ _ ____ _ _ _ +/ ___| |_ _ ___| |_ ___ _ __ / ___|___ ___| | ___ __ (_) |_ | | | | | | / __| __/ _ \ '__| | / _ \ / __| |/ / '_ \| | __| -| |___| | |_| \__ \ || __/ | | |__| (_) | (__| <| |_) | | |_ +| |___| | |_| \__ \ || __/ | | |__| (_) | (__| <| |_) | | |_ \____|_|\__,_|___/\__\___|_| \____\___/ \___|_|\_\ .__/|_|\__| - |_| + |_| ` var ( @@ -226,18 +226,19 @@ func main() { } r := mux.NewRouter() + buildInfo := web.Build{Version: version, Hash: hash, Buildtime: buildTime} r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") - web.RenderTemplate(rw, r, "login.tmpl", &web.Page{Title: "Login"}) + web.RenderTemplate(rw, r, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo}) }).Methods(http.MethodGet) r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") - web.RenderTemplate(rw, r, "imprint.tmpl", &web.Page{Title: "Imprint"}) + web.RenderTemplate(rw, r, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo}) }) r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) { rw.Header().Add("Content-Type", "text/html; charset=utf-8") - web.RenderTemplate(rw, r, "privacy.tmpl", &web.Page{Title: "Privacy"}) + web.RenderTemplate(rw, r, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo}) }) // Some routes, such as /login or /query, should only be accessible to a user that is logged in. @@ -256,6 +257,7 @@ func main() { web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ Title: "Login failed - ClusterCockpit", Error: err.Error(), + Build: buildInfo, }) })).Methods(http.MethodPost) @@ -265,6 +267,7 @@ func main() { web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ Title: "Bye - ClusterCockpit", Info: "Logout sucessful", + Build: buildInfo, }) }))).Methods(http.MethodPost) @@ -279,6 +282,7 @@ func main() { web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ Title: "Authentication failed - ClusterCockpit", Error: err.Error(), + Build: buildInfo, }) }) }) @@ -287,7 +291,7 @@ func main() { if flagDev { r.Handle("/playground", playground.Handler("GraphQL playground", "/query")) r.PathPrefix("/swagger/").Handler(httpSwagger.Handler( - httpSwagger.URL("http://localhost:8080/swagger/doc.json"))).Methods(http.MethodGet) + httpSwagger.URL("http://" + config.Keys.Addr + "/swagger/doc.json"))).Methods(http.MethodGet) } secured.Handle("/query", graphQLEndpoint) @@ -316,7 +320,7 @@ func main() { }) // Mount all /monitoring/... and /api/... routes. - routerConfig.SetupRoutes(secured) + routerConfig.SetupRoutes(secured, version, hash, buildTime) api.MountRoutes(secured) if config.Keys.EmbedStaticFiles { diff --git a/gqlgen.yml b/gqlgen.yml index acdf882..1dcf955 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -79,3 +79,4 @@ models: FilterRanges: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.FilterRanges" } SubCluster: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.SubCluster" } StatsSeries: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.StatsSeries" } + Unit: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.Unit" } diff --git a/internal/config/config.go b/internal/config/config.go index ab996ff..ca78495 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,7 +14,7 @@ import ( ) var Keys schema.ProgramConfig = schema.ProgramConfig{ - Addr: ":8080", + Addr: "localhost:8080", DisableAuthentication: false, EmbedStaticFiles: true, DBDriver: "sqlite3", diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index a83998b..cf06159 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -251,6 +251,11 @@ type ComplexityRoot struct { Socket func(childComplexity int) int } + Unit struct { + Base func(childComplexity int) int + Prefix func(childComplexity int) int + } + User struct { Email func(childComplexity int) int Name func(childComplexity int) int @@ -1275,6 +1280,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Topology.Socket(childComplexity), true + case "Unit.base": + if e.complexity.Unit.Base == nil { + break + } + + return e.complexity.Unit.Base(childComplexity), true + + case "Unit.prefix": + if e.complexity.Unit.Prefix == nil { + break + } + + return e.complexity.Unit.Prefix(childComplexity), true + case "User.email": if e.complexity.User.Email == nil { break @@ -1450,7 +1469,7 @@ type SubClusterConfig { type MetricConfig { name: String! - unit: String! + unit: Unit scope: MetricScope! aggregation: String timestep: Int! @@ -1480,7 +1499,7 @@ type JobMetricWithName { } type JobMetric { - unit: String! + unit: Unit scope: MetricScope! timestep: Int! series: [Series!] @@ -1494,6 +1513,11 @@ type Series { data: [NullableFloat!]! } +type Unit { + base: String! + prefix: String +} + type MetricStatistics { avg: Float! min: Float! @@ -3862,14 +3886,11 @@ func (ec *executionContext) _JobMetric_unit(ctx context.Context, field graphql.C return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.(string) + res := resTmp.(schema.Unit) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOUnit2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋpkgᚋschemaᚐUnit(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_JobMetric_unit(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -3879,7 +3900,13 @@ func (ec *executionContext) fieldContext_JobMetric_unit(ctx context.Context, fie IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + switch field.Name { + case "base": + return ec.fieldContext_Unit_base(ctx, field) + case "prefix": + return ec.fieldContext_Unit_prefix(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Unit", field.Name) }, } return fc, nil @@ -4771,14 +4798,11 @@ func (ec *executionContext) _MetricConfig_unit(ctx context.Context, field graphq return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.(string) + res := resTmp.(schema.Unit) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOUnit2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋpkgᚋschemaᚐUnit(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_MetricConfig_unit(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -4788,7 +4812,13 @@ func (ec *executionContext) fieldContext_MetricConfig_unit(ctx context.Context, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") + switch field.Name { + case "base": + return ec.fieldContext_Unit_base(ctx, field) + case "prefix": + return ec.fieldContext_Unit_prefix(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Unit", field.Name) }, } return fc, nil @@ -8351,6 +8381,91 @@ func (ec *executionContext) fieldContext_Topology_accelerators(ctx context.Conte return fc, nil } +func (ec *executionContext) _Unit_base(ctx context.Context, field graphql.CollectedField, obj *schema.Unit) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Unit_base(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.Base, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Unit_base(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Unit", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Unit_prefix(ctx context.Context, field graphql.CollectedField, obj *schema.Unit) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Unit_prefix(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.Prefix, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Unit_prefix(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Unit", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _User_username(ctx context.Context, field graphql.CollectedField, obj *model.User) (ret graphql.Marshaler) { fc, err := ec.fieldContext_User_username(ctx, field) if err != nil { @@ -11130,9 +11245,6 @@ func (ec *executionContext) _JobMetric(ctx context.Context, sel ast.SelectionSet out.Values[i] = ec._JobMetric_unit(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } case "scope": out.Values[i] = ec._JobMetric_scope(ctx, field, obj) @@ -11332,9 +11444,6 @@ func (ec *executionContext) _MetricConfig(ctx context.Context, sel ast.Selection out.Values[i] = ec._MetricConfig_unit(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } case "scope": out.Values[i] = ec._MetricConfig_scope(ctx, field, obj) @@ -12285,6 +12394,38 @@ func (ec *executionContext) _Topology(ctx context.Context, sel ast.SelectionSet, return out } +var unitImplementors = []string{"Unit"} + +func (ec *executionContext) _Unit(ctx context.Context, sel ast.SelectionSet, obj *schema.Unit) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, unitImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Unit") + case "base": + + out.Values[i] = ec._Unit_base(ctx, field, obj) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "prefix": + + out.Values[i] = ec._Unit_prefix(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var userImplementors = []string{"User"} func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *model.User) graphql.Marshaler { @@ -14620,6 +14761,10 @@ func (ec *executionContext) unmarshalOTimeRange2ᚖgithubᚗcomᚋClusterCockpit return &res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) marshalOUnit2githubᚗcomᚋClusterCockpitᚋccᚑbackendᚋpkgᚋschemaᚐUnit(ctx context.Context, sel ast.SelectionSet, v schema.Unit) graphql.Marshaler { + return ec._Unit(ctx, sel, &v) +} + func (ec *executionContext) marshalOUser2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐUser(ctx context.Context, sel ast.SelectionSet, v *model.User) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/internal/repository/init.go b/internal/repository/init.go index 554d88b..e813b44 100644 --- a/internal/repository/init.go +++ b/internal/repository/init.go @@ -389,10 +389,11 @@ func checkJobData(d *schema.JobData) error { for _, s := range metric.Series { fp := schema.ConvertFloatToFloat64(s.Data) // Normalize values with new unit prefix - units.NormalizeSeries(fp, avg, metric.Unit, &newUnit) + oldUnit := metric.Unit.Base + units.NormalizeSeries(fp, avg, oldUnit, &newUnit) s.Data = schema.GetFloat64ToFloat(fp) } - metric.Unit = newUnit + metric.Unit.Base = newUnit } } } diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go index a5ea524..9424df7 100644 --- a/internal/routerConfig/routes.go +++ b/internal/routerConfig/routes.go @@ -253,7 +253,7 @@ func buildFilterPresets(query url.Values) map[string]interface{} { return filterPresets } -func SetupRoutes(router *mux.Router) { +func SetupRoutes(router *mux.Router, version string, hash string, buildTime string) { userCfgRepo := repository.GetUserCfgRepo() for _, route := range routes { route := route @@ -271,7 +271,7 @@ func SetupRoutes(router *mux.Router) { } username, isAdmin, isSupporter := "", true, true - + if user := auth.GetUser(r.Context()); user != nil { username = user.Username isAdmin = user.HasRole(auth.RoleAdmin) @@ -281,6 +281,7 @@ func SetupRoutes(router *mux.Router) { page := web.Page{ Title: title, User: web.User{Username: username, IsAdmin: isAdmin, IsSupporter: isSupporter}, + Build: web.Build{Version: version, Hash: hash, Buildtime: buildTime}, Config: conf, Infos: infos, } diff --git a/web/frontend/public/global.css b/web/frontend/public/global.css index 8feecf6..7e4e805 100644 --- a/web/frontend/public/global.css +++ b/web/frontend/public/global.css @@ -52,3 +52,21 @@ footer { margin: 0rem 0.8rem; white-space: nowrap; } + +.build-list { + color: gray; + font-size: 12px; + list-style-type: none; + padding-left: 0; + width: 100%; + display: flex; + flex-wrap: wrap; + justify-content: right; + margin-top: 0px; + margin-bottom: 5px; +} + +.build-list-item { + margin: 0rem 0.8rem; + white-space: nowrap; +} diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte index 26842c8..0153b18 100644 --- a/web/frontend/src/Status.root.svelte +++ b/web/frontend/src/Status.root.svelte @@ -2,8 +2,8 @@ import Refresher from './joblist/Refresher.svelte' import Roofline, { transformPerNodeData } from './plots/Roofline.svelte' import Histogram from './plots/Histogram.svelte' - import { Row, Col, Spinner, Card, Table, Progress } from 'sveltestrap' - import { init } from './utils.js' + import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap' + import { init, formatNumber } from './utils.js' import { operationStore, query } from '@urql/svelte' const { query: initq } = init() @@ -60,7 +60,12 @@ query(mainQuery) + + + +

Current usage of cluster "{cluster}"

+ {#if $initq.fetching || $mainQuery.fetching} @@ -89,54 +94,72 @@
{/if} + +
+ + + {#if $initq.data && $mainQuery.data} {#each $initq.data.clusters.find(c => c.name == cluster).subClusters as subCluster, i} - - - - - - - - - - - - - - - - - - - - - - -
SubCluster{subCluster.name}
Allocated Nodes
({allocatedNodes[subCluster.name]} / {subCluster.numberOfNodes})
Flop Rate
({flopRate[subCluster.name]} / {subCluster.flopRateSimd * subCluster.numberOfNodes})
MemBw Rate
({memBwRate[subCluster.name]} / {subCluster.memoryBandwidth * subCluster.numberOfNodes})
+ + + + + SubCluster "{subCluster.name}" + + + + + + + + + + + + + + + + + + +
Allocated Nodes
({allocatedNodes[subCluster.name]} Nodes / {subCluster.numberOfNodes} Total Nodes)
Flop Rate (Any)
({formatNumber(flopRate[subCluster.name])}Flops/s / {formatNumber((subCluster.flopRateSimd * subCluster.numberOfNodes))}Flops/s [Max])
MemBw Rate
({formatNumber(memBwRate[subCluster.name])}Byte/s / {formatNumber((subCluster.memoryBandwidth * subCluster.numberOfNodes))}Byte/s [Max])
+
+
+ + +
+ {#key $mainQuery.data.nodeMetrics} + data.subCluster == subCluster.name))} /> + {/key} +
-
- {#key $mainQuery.data.nodeMetrics} - data.subCluster == subCluster.name))} /> - {/key} -
{/each} - -
-

Top Users

- {#key $mainQuery.data} - b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} - label={(x) => x < $mainQuery.data.topUsers.length ? $mainQuery.data.topUsers[Math.floor(x)].name : '0'} /> - {/key} -
-
+ +
+ + + + + +
+

Top Users

+ {#key $mainQuery.data} + b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} + label={(x) => x < $mainQuery.data.topUsers.length ? $mainQuery.data.topUsers[Math.floor(x)].name : '0'} + xlabel="User Name" ylabel="Number of Jobs" /> + {/key} +
+ + - + {#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }} @@ -144,41 +167,46 @@ {/each}
NameNumber of Nodes
User NameNumber of Nodes
{name}
-
-
-

Top Projects

+ + +

Top Projects

{#key $mainQuery.data} b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} - label={(x) => x < $mainQuery.data.topProjects.length ? $mainQuery.data.topProjects[Math.floor(x)].name : '0'} /> + label={(x) => x < $mainQuery.data.topProjects.length ? $mainQuery.data.topProjects[Math.floor(x)].name : '0'} + xlabel="Project Code" ylabel="Number of Jobs" /> {/key} -
-
+ + - + {#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }} {/each}
NameNumber of Nodes
Project CodeNumber of Nodes
{name}{count}
-
+
- -
-

Duration Distribution

+ + +
+

Duration Distribution

+ {#key $mainQuery.data.stats} + + {/key} +
+ + +

Number of Nodes Distribution

{#key $mainQuery.data.stats} + data={$mainQuery.data.stats[0].histNumNodes} + xlabel="Allocated Nodes" ylabel="Number of Jobs" /> {/key} -
-
-

Number of Nodes Distribution

- {#key $mainQuery.data.stats} - - {/key} -
+
{/if} diff --git a/web/frontend/src/plots/Histogram.svelte b/web/frontend/src/plots/Histogram.svelte index c00de12..b114f07 100644 --- a/web/frontend/src/plots/Histogram.svelte +++ b/web/frontend/src/plots/Histogram.svelte @@ -1,4 +1,4 @@ -