Merge pull request #54 from ClusterCockpit/48_improve_status_view

48 improve status view
This commit is contained in:
Jan Eitzinger 2022-10-17 10:10:49 +02:00 committed by GitHub
commit 8227f904f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 354 additions and 120 deletions

View File

@ -77,7 +77,7 @@ type SubClusterConfig {
type MetricConfig { type MetricConfig {
name: String! name: String!
unit: String! unit: Unit
scope: MetricScope! scope: MetricScope!
aggregation: String aggregation: String
timestep: Int! timestep: Int!
@ -107,7 +107,7 @@ type JobMetricWithName {
} }
type JobMetric { type JobMetric {
unit: String! unit: Unit
scope: MetricScope! scope: MetricScope!
timestep: Int! timestep: Int!
series: [Series!] series: [Series!]
@ -121,6 +121,11 @@ type Series {
data: [NullableFloat!]! data: [NullableFloat!]!
} }
type Unit {
base: String!
prefix: String
}
type MetricStatistics { type MetricStatistics {
avg: Float! avg: Float!
min: Float! min: Float!

View File

@ -226,18 +226,19 @@ func main() {
} }
r := mux.NewRouter() r := mux.NewRouter()
buildInfo := web.Build{Version: version, Hash: hash, Buildtime: buildTime}
r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8") 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) }).Methods(http.MethodGet)
r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8") 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) { r.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("Content-Type", "text/html; charset=utf-8") 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. // 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{ web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
Title: "Login failed - ClusterCockpit", Title: "Login failed - ClusterCockpit",
Error: err.Error(), Error: err.Error(),
Build: buildInfo,
}) })
})).Methods(http.MethodPost) })).Methods(http.MethodPost)
@ -265,6 +267,7 @@ func main() {
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
Title: "Bye - ClusterCockpit", Title: "Bye - ClusterCockpit",
Info: "Logout sucessful", Info: "Logout sucessful",
Build: buildInfo,
}) })
}))).Methods(http.MethodPost) }))).Methods(http.MethodPost)
@ -279,6 +282,7 @@ func main() {
web.RenderTemplate(rw, r, "login.tmpl", &web.Page{ web.RenderTemplate(rw, r, "login.tmpl", &web.Page{
Title: "Authentication failed - ClusterCockpit", Title: "Authentication failed - ClusterCockpit",
Error: err.Error(), Error: err.Error(),
Build: buildInfo,
}) })
}) })
}) })
@ -287,7 +291,7 @@ func main() {
if flagDev { if flagDev {
r.Handle("/playground", playground.Handler("GraphQL playground", "/query")) r.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
r.PathPrefix("/swagger/").Handler(httpSwagger.Handler( 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) secured.Handle("/query", graphQLEndpoint)
@ -316,7 +320,7 @@ func main() {
}) })
// Mount all /monitoring/... and /api/... routes. // Mount all /monitoring/... and /api/... routes.
routerConfig.SetupRoutes(secured) routerConfig.SetupRoutes(secured, version, hash, buildTime)
api.MountRoutes(secured) api.MountRoutes(secured)
if config.Keys.EmbedStaticFiles { if config.Keys.EmbedStaticFiles {

View File

@ -79,3 +79,4 @@ models:
FilterRanges: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.FilterRanges" } FilterRanges: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.FilterRanges" }
SubCluster: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.SubCluster" } SubCluster: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.SubCluster" }
StatsSeries: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.StatsSeries" } StatsSeries: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.StatsSeries" }
Unit: { model: "github.com/ClusterCockpit/cc-backend/pkg/schema.Unit" }

View File

@ -14,7 +14,7 @@ import (
) )
var Keys schema.ProgramConfig = schema.ProgramConfig{ var Keys schema.ProgramConfig = schema.ProgramConfig{
Addr: ":8080", Addr: "localhost:8080",
DisableAuthentication: false, DisableAuthentication: false,
EmbedStaticFiles: true, EmbedStaticFiles: true,
DBDriver: "sqlite3", DBDriver: "sqlite3",

View File

@ -251,6 +251,11 @@ type ComplexityRoot struct {
Socket func(childComplexity int) int Socket func(childComplexity int) int
} }
Unit struct {
Base func(childComplexity int) int
Prefix func(childComplexity int) int
}
User struct { User struct {
Email func(childComplexity int) int Email func(childComplexity int) int
Name 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 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": case "User.email":
if e.complexity.User.Email == nil { if e.complexity.User.Email == nil {
break break
@ -1450,7 +1469,7 @@ type SubClusterConfig {
type MetricConfig { type MetricConfig {
name: String! name: String!
unit: String! unit: Unit
scope: MetricScope! scope: MetricScope!
aggregation: String aggregation: String
timestep: Int! timestep: Int!
@ -1480,7 +1499,7 @@ type JobMetricWithName {
} }
type JobMetric { type JobMetric {
unit: String! unit: Unit
scope: MetricScope! scope: MetricScope!
timestep: Int! timestep: Int!
series: [Series!] series: [Series!]
@ -1494,6 +1513,11 @@ type Series {
data: [NullableFloat!]! data: [NullableFloat!]!
} }
type Unit {
base: String!
prefix: String
}
type MetricStatistics { type MetricStatistics {
avg: Float! avg: Float!
min: Float! min: Float!
@ -3862,14 +3886,11 @@ func (ec *executionContext) _JobMetric_unit(ctx context.Context, field graphql.C
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(string) res := resTmp.(schema.Unit)
fc.Result = res 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) { 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, IsMethod: false,
IsResolver: false, IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { 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 return fc, nil
@ -4771,14 +4798,11 @@ func (ec *executionContext) _MetricConfig_unit(ctx context.Context, field graphq
return graphql.Null return graphql.Null
} }
if resTmp == nil { if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null return graphql.Null
} }
res := resTmp.(string) res := resTmp.(schema.Unit)
fc.Result = res 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) { 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, IsMethod: false,
IsResolver: false, IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { 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 return fc, nil
@ -8351,6 +8381,91 @@ func (ec *executionContext) fieldContext_Topology_accelerators(ctx context.Conte
return fc, nil 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) { 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) fc, err := ec.fieldContext_User_username(ctx, field)
if err != nil { 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) out.Values[i] = ec._JobMetric_unit(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "scope": case "scope":
out.Values[i] = ec._JobMetric_scope(ctx, field, obj) 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) out.Values[i] = ec._MetricConfig_unit(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "scope": case "scope":
out.Values[i] = ec._MetricConfig_scope(ctx, field, obj) 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 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"} var userImplementors = []string{"User"}
func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *model.User) graphql.Marshaler { 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) 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 { 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 { if v == nil {
return graphql.Null return graphql.Null

View File

@ -389,10 +389,11 @@ func checkJobData(d *schema.JobData) error {
for _, s := range metric.Series { for _, s := range metric.Series {
fp := schema.ConvertFloatToFloat64(s.Data) fp := schema.ConvertFloatToFloat64(s.Data)
// Normalize values with new unit prefix // 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) s.Data = schema.GetFloat64ToFloat(fp)
} }
metric.Unit = newUnit metric.Unit.Base = newUnit
} }
} }
} }

View File

@ -253,7 +253,7 @@ func buildFilterPresets(query url.Values) map[string]interface{} {
return filterPresets return filterPresets
} }
func SetupRoutes(router *mux.Router) { func SetupRoutes(router *mux.Router, version string, hash string, buildTime string) {
userCfgRepo := repository.GetUserCfgRepo() userCfgRepo := repository.GetUserCfgRepo()
for _, route := range routes { for _, route := range routes {
route := route route := route
@ -281,6 +281,7 @@ func SetupRoutes(router *mux.Router) {
page := web.Page{ page := web.Page{
Title: title, Title: title,
User: web.User{Username: username, IsAdmin: isAdmin, IsSupporter: isSupporter}, User: web.User{Username: username, IsAdmin: isAdmin, IsSupporter: isSupporter},
Build: web.Build{Version: version, Hash: hash, Buildtime: buildTime},
Config: conf, Config: conf,
Infos: infos, Infos: infos,
} }

View File

@ -52,3 +52,21 @@ footer {
margin: 0rem 0.8rem; margin: 0rem 0.8rem;
white-space: nowrap; 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;
}

View File

@ -2,8 +2,8 @@
import Refresher from './joblist/Refresher.svelte' import Refresher from './joblist/Refresher.svelte'
import Roofline, { transformPerNodeData } from './plots/Roofline.svelte' import Roofline, { transformPerNodeData } from './plots/Roofline.svelte'
import Histogram from './plots/Histogram.svelte' import Histogram from './plots/Histogram.svelte'
import { Row, Col, Spinner, Card, Table, Progress } from 'sveltestrap' import { Row, Col, Spinner, Card, CardHeader, CardTitle, CardBody, Table, Progress, Icon } from 'sveltestrap'
import { init } from './utils.js' import { init, formatNumber } from './utils.js'
import { operationStore, query } from '@urql/svelte' import { operationStore, query } from '@urql/svelte'
const { query: initq } = init() const { query: initq } = init()
@ -60,7 +60,12 @@
query(mainQuery) query(mainQuery)
</script> </script>
<!-- Loading indicator & Refresh -->
<Row> <Row>
<Col xs="auto" style="align-self: flex-end;">
<h4 class="mb-0" >Current usage of cluster "{cluster}"</h4>
</Col>
<Col xs="auto"> <Col xs="auto">
{#if $initq.fetching || $mainQuery.fetching} {#if $initq.fetching || $mainQuery.fetching}
<Spinner/> <Spinner/>
@ -89,54 +94,72 @@
</Col> </Col>
</Row> </Row>
{/if} {/if}
<hr>
<!-- Gauges & Roofline per Subcluster-->
{#if $initq.data && $mainQuery.data} {#if $initq.data && $mainQuery.data}
{#each $initq.data.clusters.find(c => c.name == cluster).subClusters as subCluster, i} {#each $initq.data.clusters.find(c => c.name == cluster).subClusters as subCluster, i}
<Row> <Row cols={2} class="mb-3 justify-content-center">
<Col xs="3"> <Col xs="4" class="px-3">
<Card class="h-auto mt-1">
<CardHeader>
<CardTitle class="mb-0">SubCluster "{subCluster.name}"</CardTitle>
</CardHeader>
<CardBody>
<Table> <Table>
<tr>
<th scope="col">SubCluster</th>
<td colspan="2">{subCluster.name}</td>
</tr>
<tr> <tr>
<th scope="col">Allocated Nodes</th> <th scope="col">Allocated Nodes</th>
<td style="min-width: 75px;"><div class="col"><Progress value={allocatedNodes[subCluster.name]} max={subCluster.numberOfNodes}/></div></td> <td style="min-width: 100px;"><div class="col"><Progress value={allocatedNodes[subCluster.name]} max={subCluster.numberOfNodes}/></div></td>
<td>({allocatedNodes[subCluster.name]} / {subCluster.numberOfNodes})</td> <td>({allocatedNodes[subCluster.name]} Nodes / {subCluster.numberOfNodes} Total Nodes)</td>
</tr> </tr>
<tr> <tr>
<th scope="col">Flop Rate</th> <th scope="col">Flop Rate (Any) <Icon name="info-circle" class="p-1" style="cursor: help;" title="Flops[Any] = (Flops[Double] x 2) + Flops[Single]"/></th>
<td style="min-width: 75px;"><div class="col"><Progress value={flopRate[subCluster.name]} max={subCluster.flopRateSimd * subCluster.numberOfNodes}/></div></td> <td style="min-width: 100px;"><div class="col"><Progress value={flopRate[subCluster.name]} max={subCluster.flopRateSimd * subCluster.numberOfNodes}/></div></td>
<td>({flopRate[subCluster.name]} / {subCluster.flopRateSimd * subCluster.numberOfNodes})</td> <td>({formatNumber(flopRate[subCluster.name])}Flops/s / {formatNumber((subCluster.flopRateSimd * subCluster.numberOfNodes))}Flops/s [Max])</td>
</tr> </tr>
<tr> <tr>
<th scope="col">MemBw Rate</th> <th scope="col">MemBw Rate</th>
<td style="min-width: 75px;"><div class="col"><Progress value={memBwRate[subCluster.name]} max={subCluster.memoryBandwidth * subCluster.numberOfNodes}/></div></td> <td style="min-width: 100px;"><div class="col"><Progress value={memBwRate[subCluster.name]} max={subCluster.memoryBandwidth * subCluster.numberOfNodes}/></div></td>
<td>({memBwRate[subCluster.name]} / {subCluster.memoryBandwidth * subCluster.numberOfNodes})</td> <td>({formatNumber(memBwRate[subCluster.name])}Byte/s / {formatNumber((subCluster.memoryBandwidth * subCluster.numberOfNodes))}Byte/s [Max])</td>
</tr> </tr>
</Table> </Table>
</CardBody>
</Card>
</Col> </Col>
<div class="col-9" bind:clientWidth={plotWidths[i]}> <Col class="px-3">
<div bind:clientWidth={plotWidths[i]}>
{#key $mainQuery.data.nodeMetrics} {#key $mainQuery.data.nodeMetrics}
<Roofline <Roofline
width={plotWidths[i] - 10} height={300} colorDots={false} cluster={subCluster} width={plotWidths[i] - 10} height={300} colorDots={true} showTime={false} cluster={subCluster}
data={transformPerNodeData($mainQuery.data.nodeMetrics.filter(data => data.subCluster == subCluster.name))} /> data={transformPerNodeData($mainQuery.data.nodeMetrics.filter(data => data.subCluster == subCluster.name))} />
{/key} {/key}
</div> </div>
</Col>
</Row> </Row>
{/each} {/each}
<Row>
<div class="col-4" bind:clientWidth={colWidth1}> <hr style="margin-top: -1em;">
<h4>Top Users</h4>
<!-- Usage Stats as Histograms -->
<Row cols={4}>
<Col class="p-2">
<div bind:clientWidth={colWidth1}>
<h4 class="mb-3 text-center">Top Users</h4>
{#key $mainQuery.data} {#key $mainQuery.data}
<Histogram <Histogram
width={colWidth1 - 25} height={300} width={colWidth1 - 25} height={300}
data={$mainQuery.data.topUsers.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} data={$mainQuery.data.topUsers.sort((a, b) => 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'} /> label={(x) => x < $mainQuery.data.topUsers.length ? $mainQuery.data.topUsers[Math.floor(x)].name : '0'}
xlabel="User Name" ylabel="Number of Jobs" />
{/key} {/key}
</div> </div>
<div class="col-2"> </Col>
<Col class="px-4 py-2">
<Table> <Table>
<tr><th>Name</th><th>Number of Nodes</th></tr> <tr class="mb-2"><th>User Name</th><th>Number of Nodes</th></tr>
{#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }} {#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }}
<tr> <tr>
<th scope="col"><a href="/monitoring/user/{name}">{name}</a></th> <th scope="col"><a href="/monitoring/user/{name}">{name}</a></th>
@ -144,41 +167,46 @@
</tr> </tr>
{/each} {/each}
</Table> </Table>
</div> </Col>
<div class="col-4"> <Col class="p-2">
<h4>Top Projects</h4> <h4 class="mb-3 text-center">Top Projects</h4>
{#key $mainQuery.data} {#key $mainQuery.data}
<Histogram <Histogram
width={colWidth1 - 25} height={300} width={colWidth1 - 25} height={300}
data={$mainQuery.data.topProjects.sort((a, b) => b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} data={$mainQuery.data.topProjects.sort((a, b) => 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} {/key}
</div> </Col>
<div class="col-2"> <Col class="px-4 py-2">
<Table> <Table>
<tr><th>Name</th><th>Number of Nodes</th></tr> <tr class="mb-2"><th>Project Code</th><th>Number of Nodes</th></tr>
{#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }} {#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }}
<tr><th scope="col">{name}</th><td>{count}</td></tr> <tr><th scope="col">{name}</th><td>{count}</td></tr>
{/each} {/each}
</Table> </Table>
</div> </Col>
</Row> </Row>
<Row> <Row cols={2} class="mt-3">
<div class="col" bind:clientWidth={colWidth2}> <Col class="p-2">
<h4>Duration Distribution</h4> <div bind:clientWidth={colWidth2}>
<h4 class="mb-3 text-center">Duration Distribution</h4>
{#key $mainQuery.data.stats} {#key $mainQuery.data.stats}
<Histogram <Histogram
width={colWidth2 - 25} height={300} width={colWidth2 - 25} height={300}
data={$mainQuery.data.stats[0].histDuration} /> data={$mainQuery.data.stats[0].histDuration}
xlabel="Current Runtime in Hours [h]" ylabel="Number of Jobs" />
{/key} {/key}
</div> </div>
<div class="col"> </Col>
<h4>Number of Nodes Distribution</h4> <Col class="p-2">
<h4 class="mb-3 text-center">Number of Nodes Distribution</h4>
{#key $mainQuery.data.stats} {#key $mainQuery.data.stats}
<Histogram <Histogram
width={colWidth2 - 25} height={300} width={colWidth2 - 25} height={300}
data={$mainQuery.data.stats[0].histNumNodes} /> data={$mainQuery.data.stats[0].histNumNodes}
xlabel="Allocated Nodes" ylabel="Number of Jobs" />
{/key} {/key}
</div> </Col>
</Row> </Row>
{/if} {/if}

View File

@ -20,6 +20,8 @@
export let data export let data
export let width export let width
export let height export let height
export let xlabel
export let ylabel
export let min = null export let min = null
export let max = null export let max = null
export let label = formatNumber export let label = formatNumber
@ -72,9 +74,11 @@
} }
function render() { function render() {
const h = height - paddingTop - paddingBottom const labelOffset = Math.floor(height * 0.1)
const h = height - paddingTop - paddingBottom - labelOffset
const w = width - paddingLeft - paddingRight const w = width - paddingLeft - paddingRight
const barWidth = Math.ceil(w / (maxValue + 1)) const barGap = 5
const barWidth = Math.ceil(w / (maxValue + 1)) - barGap
if (Number.isNaN(barWidth)) if (Number.isNaN(barWidth))
return return
@ -83,9 +87,14 @@
const getCanvasY = (count) => (h - (count / maxCount) * h) + paddingTop const getCanvasY = (count) => (h - (count / maxCount) * h) + paddingTop
// X Axis // X Axis
ctx.font = `${fontSize}px ${fontFamily}` ctx.font = `bold ${fontSize}px ${fontFamily}`
ctx.fillStyle = 'black' ctx.fillStyle = 'black'
if (xlabel != '') {
let textWidth = ctx.measureText(xlabel).width
ctx.fillText(xlabel, Math.floor((width / 2) - (textWidth / 2) + barGap), height - Math.floor(labelOffset / 2))
}
ctx.textAlign = 'center' ctx.textAlign = 'center'
ctx.font = `${fontSize}px ${fontFamily}`
if (min != null && max != null) { if (min != null && max != null) {
const stepsizeX = getStepSize(max - min, w, 75) const stepsizeX = getStepSize(max - min, w, 75)
let startX = 0 let startX = 0
@ -94,19 +103,28 @@
for (let x = startX; x < max; x += stepsizeX) { for (let x = startX; x < max; x += stepsizeX) {
let px = ((x - min) / (max - min)) * (w - barWidth) + paddingLeft + (barWidth / 2.) let px = ((x - min) / (max - min)) * (w - barWidth) + paddingLeft + (barWidth / 2.)
ctx.fillText(`${formatNumber(x)}`, px, height - paddingBottom + 15) ctx.fillText(`${formatNumber(x)}`, px, height - paddingBottom - Math.floor(labelOffset / 2))
} }
} else { } else {
const stepsizeX = getStepSize(maxValue, w, 120) const stepsizeX = getStepSize(maxValue, w, 120)
for (let x = 0; x <= maxValue; x += stepsizeX) { for (let x = 0; x <= maxValue; x += stepsizeX) {
ctx.fillText(label(x), getCanvasX(x), height - paddingBottom + 15) ctx.fillText(label(x), getCanvasX(x), height - paddingBottom - Math.floor(labelOffset / 2))
} }
} }
// Y Axis // Y Axis
ctx.fillStyle = 'black' ctx.fillStyle = 'black'
ctx.strokeStyle = '#bbbbbb' ctx.strokeStyle = '#bbbbbb'
ctx.font = `bold ${fontSize}px ${fontFamily}`
if (ylabel != '') {
ctx.save()
ctx.translate(15, Math.floor(h / 2))
ctx.rotate(-Math.PI / 2)
ctx.fillText(ylabel, 0, 0)
ctx.restore()
}
ctx.textAlign = 'right' ctx.textAlign = 'right'
ctx.font = `${fontSize}px ${fontFamily}`
ctx.beginPath() ctx.beginPath()
const stepsizeY = getStepSize(maxCount, h, 50) const stepsizeY = getStepSize(maxCount, h, 50)
for (let y = stepsizeY; y <= maxCount; y += stepsizeY) { for (let y = stepsizeY; y <= maxCount; y += stepsizeY) {
@ -118,7 +136,7 @@
ctx.stroke() ctx.stroke()
// Draw bars // Draw bars
ctx.fillStyle = '#0066cc' ctx.fillStyle = '#85abce'
for (let p of data) { for (let p of data) {
ctx.fillRect( ctx.fillRect(
getCanvasX(p.value) - (barWidth / 2.), getCanvasX(p.value) - (barWidth / 2.),
@ -130,10 +148,10 @@
// Fat lines left and below plotting area // Fat lines left and below plotting area
ctx.strokeStyle = 'black' ctx.strokeStyle = 'black'
ctx.beginPath() ctx.beginPath()
ctx.moveTo(0, height - paddingBottom) ctx.moveTo(0, height - paddingBottom - labelOffset)
ctx.lineTo(width, height - paddingBottom) ctx.lineTo(width, height - paddingBottom - labelOffset)
ctx.moveTo(paddingLeft, 0) ctx.moveTo(paddingLeft, 0)
ctx.lineTo(paddingLeft, height- paddingBottom) ctx.lineTo(paddingLeft, height - Math.floor(labelOffset / 2))
ctx.stroke() ctx.stroke()
} }

View File

@ -67,7 +67,7 @@
return 2 return 2
} }
function render(ctx, data, cluster, width, height, colorDots, defaultMaxY) { function render(ctx, data, cluster, width, height, colorDots, showTime, defaultMaxY) {
if (width <= 0) if (width <= 0)
return return
@ -222,8 +222,8 @@
} }
ctx.stroke() ctx.stroke()
if (colorDots && data.x && data.y) { if (colorDots && showTime && data.x && data.y) {
// The Color Scale // The Color Scale For Time Information
ctx.fillStyle = 'black' ctx.fillStyle = 'black'
ctx.fillText('Time:', 17, height - 5) ctx.fillText('Time:', 17, height - 5)
const start = paddingLeft + 5 const start = paddingLeft + 5
@ -305,6 +305,7 @@
export let height export let height
export let tiles = null export let tiles = null
export let colorDots = true export let colorDots = true
export let showTime = true
export let data = null export let data = null
console.assert(data || tiles || (flopsAny && memBw), "you must provide flopsAny and memBw or tiles!") console.assert(data || tiles || (flopsAny && memBw), "you must provide flopsAny and memBw or tiles!")
@ -327,7 +328,7 @@
canvasElement.width = width canvasElement.width = width
canvasElement.height = height canvasElement.height = height
render(ctx, data, cluster, width, height, colorDots, maxY) render(ctx, data, cluster, width, height, colorDots, showTime, maxY)
}) })
let timeoutId = null let timeoutId = null
@ -347,7 +348,7 @@
timeoutId = null timeoutId = null
canvasElement.width = width canvasElement.width = width
canvasElement.height = height canvasElement.height = height
render(ctx, data, cluster, width, height, colorDots, maxY) render(ctx, data, cluster, width, height, colorDots, showTime, maxY)
}, 250) }, 250)
} }

View File

@ -37,7 +37,7 @@ export function init(extraInitQuery = '') {
clusters { clusters {
name, name,
metricConfig { metricConfig {
name, unit, peak, name, unit {base, prefix}, peak,
normal, caution, alert, normal, caution, alert,
timestep, scope, timestep, scope,
aggregation, aggregation,

View File

@ -40,6 +40,11 @@
<li class="footer-list-item"><a class="link-secondary fs-5" href="/imprint" title="Imprint" rel="nofollow">Imprint</a></li> <li class="footer-list-item"><a class="link-secondary fs-5" href="/imprint" title="Imprint" rel="nofollow">Imprint</a></li>
<li class="footer-list-item"><a class="link-secondary fs-5" href="/privacy" title="Privacy Policy" rel="nofollow">Privacy Policy</a></li> <li class="footer-list-item"><a class="link-secondary fs-5" href="/privacy" title="Privacy Policy" rel="nofollow">Privacy Policy</a></li>
</ul> </ul>
<ul class="build-list">
<li class="build-list-item">Version {{ .Build.Version }}</li>
<li class="build-list-item">Hash {{ .Build.Hash }}</li>
<li class="build-list-item">Built {{ .Build.Buildtime }}</li>
</ul>
</footer> </footer>
{{end}} {{end}}

View File

@ -59,11 +59,18 @@ type User struct {
IsSupporter bool IsSupporter bool
} }
type Build struct {
Version string
Hash string
Buildtime string
}
type Page struct { type Page struct {
Title string // Page title Title string // Page title
Error string // For generic use (e.g. the exact error message on /login) Error string // For generic use (e.g. the exact error message on /login)
Info string // For generic use (e.g. "Logout successfull" on /login) Info string // For generic use (e.g. "Logout successfull" on /login)
User User // Information about the currently logged in user User User // Information about the currently logged in user
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
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>)