mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2024-12-26 13:29:05 +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 {
|
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!
|
||||||
|
@ -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 {
|
||||||
|
@ -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" }
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
@ -127,7 +127,7 @@ export function formatNumber(x) {
|
|||||||
suffix = 'k'
|
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?
|
// 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="/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}}
|
||||||
|
|
||||||
|
@ -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>)
|
||||||
|
Loading…
Reference in New Issue
Block a user