mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-25 12:59:06 +01:00
Merge pull request #54 from ClusterCockpit/48_improve_status_view
48 improve status view
This commit is contained in:
commit
8227f904f8
@ -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!
|
||||
|
@ -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 {
|
||||
|
@ -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" }
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
var Keys schema.ProgramConfig = schema.ProgramConfig{
|
||||
Addr: ":8080",
|
||||
Addr: "localhost:8080",
|
||||
DisableAuthentication: false,
|
||||
EmbedStaticFiles: true,
|
||||
DBDriver: "sqlite3",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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?
|
||||
|
@ -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}}
|
||||
|
||||
|
@ -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>)
|
||||
|
Loading…
Reference in New Issue
Block a user