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}
+
+
+
- Name | Number of Nodes |
+ User Name | Number of Nodes |
{#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }}
{name} |
@@ -144,41 +167,46 @@
{/each}
-
-
-
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}
-
-
+
+
- Name | Number of Nodes |
+ Project Code | Number of Nodes |
{#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }}
{name} | {count} |
{/each}
-
+
-
-
-
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 @@
-