mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 09:35:07 +01:00 
			
		
		
		
	Merge pull request #54 from ClusterCockpit/48_improve_status_view
48 improve status view
This commit is contained in:
		@@ -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>)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user