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 {
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!

View File

@ -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 {

View File

@ -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" }

View File

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

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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,
}

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ export function init(extraInitQuery = '') {
clusters {
name,
metricConfig {
name, unit, peak,
name, unit {base, prefix}, peak,
normal, caution, alert,
timestep, scope,
aggregation,
@ -127,7 +127,7 @@ export function formatNumber(x) {
suffix = 'k'
}
return `${(Math.round(x * 100) / 100)}${suffix}`
return `${(Math.round(x * 100) / 100)} ${suffix}`
}
// Use https://developer.mozilla.org/en-US/docs/Web/API/structuredClone instead?

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="/privacy" title="Privacy Policy" rel="nofollow">Privacy Policy</a></li>
</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>
{{end}}

View File

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