mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-04-01 18:15:54 +02:00
Merge branch 'dev' into review_logging
This commit is contained in:
commit
c964f09a4f
12
configs/default_metrics.json
Normal file
12
configs/default_metrics.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"clusters": [
|
||||||
|
{
|
||||||
|
"name": "fritz",
|
||||||
|
"default_metrics": "cpu_load, flops_any, core_power, lustre_open, mem_used, mem_bw, net_bytes_in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "alex",
|
||||||
|
"default_metrics": "flops_any, mem_bw, mem_used, vectorization_ratio"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -29,6 +29,7 @@ require (
|
|||||||
golang.org/x/crypto v0.35.0
|
golang.org/x/crypto v0.35.0
|
||||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
|
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
|
||||||
golang.org/x/oauth2 v0.27.0
|
golang.org/x/oauth2 v0.27.0
|
||||||
|
golang.org/x/time v0.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
2
go.sum
2
go.sum
@ -325,6 +325,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
@ -10,11 +10,14 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/config"
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
||||||
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
"github.com/ClusterCockpit/cc-backend/pkg/log"
|
||||||
@ -32,6 +35,19 @@ var (
|
|||||||
authInstance *Authentication
|
authInstance *Authentication
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ipUserLimiters sync.Map
|
||||||
|
|
||||||
|
func getIPUserLimiter(ip, username string) *rate.Limiter {
|
||||||
|
key := ip + ":" + username
|
||||||
|
limiter, ok := ipUserLimiters.Load(key)
|
||||||
|
if !ok {
|
||||||
|
newLimiter := rate.NewLimiter(rate.Every(time.Hour/10), 10)
|
||||||
|
ipUserLimiters.Store(key, newLimiter)
|
||||||
|
return newLimiter
|
||||||
|
}
|
||||||
|
return limiter.(*rate.Limiter)
|
||||||
|
}
|
||||||
|
|
||||||
type Authentication struct {
|
type Authentication struct {
|
||||||
sessionStore *sessions.CookieStore
|
sessionStore *sessions.CookieStore
|
||||||
LdapAuth *LdapAuthenticator
|
LdapAuth *LdapAuthenticator
|
||||||
@ -88,7 +104,7 @@ func Init() {
|
|||||||
authInstance.sessionStore = sessions.NewCookieStore(bytes)
|
authInstance.sessionStore = sessions.NewCookieStore(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if d, err := time.ParseDuration(config.Keys.SessionMaxAge); err != nil {
|
if d, err := time.ParseDuration(config.Keys.SessionMaxAge); err == nil {
|
||||||
authInstance.SessionMaxAge = d
|
authInstance.SessionMaxAge = d
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,9 +224,21 @@ func (auth *Authentication) Login(
|
|||||||
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error),
|
onfailure func(rw http.ResponseWriter, r *http.Request, loginErr error),
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
username := r.FormValue("username")
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
var dbUser *schema.User
|
if err != nil {
|
||||||
|
ip = r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
username := r.FormValue("username")
|
||||||
|
|
||||||
|
limiter := getIPUserLimiter(ip, username)
|
||||||
|
if !limiter.Allow() {
|
||||||
|
log.Warnf("AUTH/RATE > Too many login attempts for combination IP: %s, Username: %s", ip, username)
|
||||||
|
onfailure(rw, r, errors.New("Too many login attempts, try again in a few minutes."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbUser *schema.User
|
||||||
if username != "" {
|
if username != "" {
|
||||||
var err error
|
var err error
|
||||||
dbUser, err = repository.GetUserRepository().GetUser(username)
|
dbUser, err = repository.GetUserRepository().GetUser(username)
|
||||||
|
44
internal/config/default_metrics.go
Normal file
44
internal/config/default_metrics.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultMetricsCluster struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DefaultMetrics string `json:"default_metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultMetricsConfig struct {
|
||||||
|
Clusters []DefaultMetricsCluster `json:"clusters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadDefaultMetricsConfig() (*DefaultMetricsConfig, error) {
|
||||||
|
filePath := "default_metrics.json"
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var cfg DefaultMetricsConfig
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMetricsString(s string) []string {
|
||||||
|
parts := strings.Split(s, ",")
|
||||||
|
var metrics []string
|
||||||
|
for _, p := range parts {
|
||||||
|
trimmed := strings.TrimSpace(p)
|
||||||
|
if trimmed != "" {
|
||||||
|
metrics = append(metrics, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metrics
|
||||||
|
}
|
@ -19,6 +19,7 @@ import (
|
|||||||
sq "github.com/Masterminds/squirrel"
|
sq "github.com/Masterminds/squirrel"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -127,6 +128,30 @@ func (r *UserRepository) AddUser(user *schema.User) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
|
log.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
|
||||||
|
|
||||||
|
defaultMetricsCfg, err := config.LoadDefaultMetricsConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error loading default metrics config: %v", err)
|
||||||
|
} else if defaultMetricsCfg != nil {
|
||||||
|
for _, cluster := range defaultMetricsCfg.Clusters {
|
||||||
|
metricsArray := config.ParseMetricsString(cluster.DefaultMetrics)
|
||||||
|
metricsJSON, err := json.Marshal(metricsArray)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error marshaling default metrics for cluster %s: %v", cluster.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
confKey := "job_view_selectedMetrics:" + cluster.Name
|
||||||
|
if _, err := sq.Insert("configuration").
|
||||||
|
Columns("username", "confkey", "value").
|
||||||
|
Values(user.Username, confKey, string(metricsJSON)).
|
||||||
|
RunWith(r.DB).Exec(); err != nil {
|
||||||
|
log.Errorf("Error inserting default job view metrics for user %s and cluster %s: %v", user.Username, cluster.Name, err)
|
||||||
|
} else {
|
||||||
|
log.Infof("Default job view metrics for user %s and cluster %s set to %s", user.Username, cluster.Name, string(metricsJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
Card,
|
Card,
|
||||||
Table,
|
Table,
|
||||||
Icon,
|
Icon,
|
||||||
|
Tooltip
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import {
|
import {
|
||||||
init,
|
init,
|
||||||
@ -70,6 +71,8 @@
|
|||||||
...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()]),
|
...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()]),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$: clusterName = cluster?.name ? cluster.name : cluster;
|
||||||
|
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ key: "totalWalltime", label: "Walltime" },
|
{ key: "totalWalltime", label: "Walltime" },
|
||||||
{ key: "totalNodeHours", label: "Node Hours" },
|
{ key: "totalNodeHours", label: "Node Hours" },
|
||||||
@ -159,6 +162,7 @@
|
|||||||
groupBy: $groupBy
|
groupBy: $groupBy
|
||||||
) {
|
) {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
totalWalltime
|
totalWalltime
|
||||||
totalNodeHours
|
totalNodeHours
|
||||||
totalCoreHours
|
totalCoreHours
|
||||||
@ -422,15 +426,22 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
|
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
|
||||||
{#if groupSelection.key == "user"}
|
{#if groupSelection.key == "user"}
|
||||||
<th scope="col"
|
<th scope="col" id="topName-{te.id}"
|
||||||
><a href="/monitoring/user/{te.id}?cluster={cluster}"
|
><a href="/monitoring/user/{te.id}?cluster={clusterName}"
|
||||||
>{te.id}</a
|
>{te.id}</a
|
||||||
></th
|
></th
|
||||||
>
|
>
|
||||||
|
{#if te?.name}
|
||||||
|
<Tooltip
|
||||||
|
target={`topName-${te.id}`}
|
||||||
|
placement="left"
|
||||||
|
>{te.name}</Tooltip
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
><a
|
><a
|
||||||
href="/monitoring/jobs/?cluster={cluster}&project={te.id}&projectMatch=eq"
|
href="/monitoring/jobs/?cluster={clusterName}&project={te.id}&projectMatch=eq"
|
||||||
>{te.id}</a
|
>{te.id}</a
|
||||||
></th
|
></th
|
||||||
>
|
>
|
||||||
|
@ -58,7 +58,8 @@
|
|||||||
let plots = {},
|
let plots = {},
|
||||||
statsTable
|
statsTable
|
||||||
|
|
||||||
let missingMetrics = [],
|
let availableMetrics = new Set(),
|
||||||
|
missingMetrics = [],
|
||||||
missingHosts = [],
|
missingHosts = [],
|
||||||
somethingMissing = false;
|
somethingMissing = false;
|
||||||
|
|
||||||
@ -128,7 +129,12 @@
|
|||||||
|
|
||||||
const pendingMetrics = [
|
const pendingMetrics = [
|
||||||
...(ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
|
...(ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
|
||||||
ccconfig[`job_view_selectedMetrics`]
|
$initq.data.globalMetrics.reduce((names, gm) => {
|
||||||
|
if (gm.availability.find((av) => av.cluster === job.cluster)) {
|
||||||
|
names.push(gm.name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}, [])
|
||||||
),
|
),
|
||||||
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
|
...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
|
||||||
ccconfig[`job_view_nodestats_selectedMetrics`]
|
ccconfig[`job_view_nodestats_selectedMetrics`]
|
||||||
@ -293,7 +299,7 @@
|
|||||||
{#if $initq.data}
|
{#if $initq.data}
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary">
|
<Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary">
|
||||||
Select Metrics
|
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
{/if}
|
{/if}
|
||||||
@ -431,6 +437,7 @@
|
|||||||
configName="job_view_selectedMetrics"
|
configName="job_view_selectedMetrics"
|
||||||
bind:metrics={selectedMetrics}
|
bind:metrics={selectedMetrics}
|
||||||
bind:isOpen={isMetricsSelectionOpen}
|
bind:isOpen={isMetricsSelectionOpen}
|
||||||
|
bind:allMetrics={availableMetrics}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
Progress,
|
Progress,
|
||||||
Icon,
|
Icon,
|
||||||
Button,
|
Button,
|
||||||
|
Tooltip
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import {
|
import {
|
||||||
queryStore,
|
queryStore,
|
||||||
@ -75,9 +76,9 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
let isHistogramSelectionOpen = false;
|
let isHistogramSelectionOpen = false;
|
||||||
$: metricsInHistograms = cluster
|
$: selectedHistograms = cluster
|
||||||
? ccconfig[`user_view_histogramMetrics:${cluster}`] || []
|
? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
|
||||||
: ccconfig.user_view_histogramMetrics || [];
|
: ccconfig['user_view_histogramMetrics'] || [];
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
// Note: nodeMetrics are requested on configured $timestep resolution
|
// Note: nodeMetrics are requested on configured $timestep resolution
|
||||||
@ -90,7 +91,7 @@
|
|||||||
$metrics: [String!]
|
$metrics: [String!]
|
||||||
$from: Time!
|
$from: Time!
|
||||||
$to: Time!
|
$to: Time!
|
||||||
$metricsInHistograms: [String!]
|
$selectedHistograms: [String!]
|
||||||
) {
|
) {
|
||||||
nodeMetrics(
|
nodeMetrics(
|
||||||
cluster: $cluster
|
cluster: $cluster
|
||||||
@ -116,7 +117,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stats: jobsStatistics(filter: $filter, metrics: $metricsInHistograms) {
|
stats: jobsStatistics(filter: $filter, metrics: $selectedHistograms) {
|
||||||
histDuration {
|
histDuration {
|
||||||
count
|
count
|
||||||
value
|
value
|
||||||
@ -157,7 +158,7 @@
|
|||||||
from: from.toISOString(),
|
from: from.toISOString(),
|
||||||
to: to.toISOString(),
|
to: to.toISOString(),
|
||||||
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
||||||
metricsInHistograms: metricsInHistograms,
|
selectedHistograms: selectedHistograms,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -177,6 +178,7 @@
|
|||||||
groupBy: USER
|
groupBy: USER
|
||||||
) {
|
) {
|
||||||
id
|
id
|
||||||
|
name
|
||||||
totalJobs
|
totalJobs
|
||||||
totalNodes
|
totalNodes
|
||||||
totalCores
|
totalCores
|
||||||
@ -515,12 +517,19 @@
|
|||||||
{#each $topUserQuery.data.topUser as tu, i}
|
{#each $topUserQuery.data.topUser as tu, i}
|
||||||
<tr>
|
<tr>
|
||||||
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
|
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
|
||||||
<th scope="col"
|
<th scope="col" id="topName-{tu.id}"
|
||||||
><a
|
><a
|
||||||
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
|
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
|
||||||
>{tu.id}</a
|
>{tu.id}</a
|
||||||
></th
|
></th
|
||||||
>
|
>
|
||||||
|
{#if tu?.name}
|
||||||
|
<Tooltip
|
||||||
|
target={`topName-${tu.id}`}
|
||||||
|
placement="left"
|
||||||
|
>{tu.name}</Tooltip
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
<td>{tu[topUserSelection.key]}</td>
|
<td>{tu[topUserSelection.key]}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
@ -652,7 +661,7 @@
|
|||||||
|
|
||||||
<!-- Selectable Stats as Histograms : Average Values of Running Jobs -->
|
<!-- Selectable Stats as Histograms : Average Values of Running Jobs -->
|
||||||
|
|
||||||
{#if metricsInHistograms}
|
{#if selectedHistograms}
|
||||||
{#key $mainQuery.data.stats[0].histMetrics}
|
{#key $mainQuery.data.stats[0].histMetrics}
|
||||||
<PlotGrid
|
<PlotGrid
|
||||||
let:item
|
let:item
|
||||||
@ -675,6 +684,6 @@
|
|||||||
|
|
||||||
<HistogramSelection
|
<HistogramSelection
|
||||||
bind:cluster
|
bind:cluster
|
||||||
bind:metricsInHistograms
|
bind:selectedHistograms
|
||||||
bind:isOpen={isHistogramSelectionOpen}
|
bind:isOpen={isHistogramSelectionOpen}
|
||||||
/>
|
/>
|
||||||
|
@ -68,16 +68,16 @@
|
|||||||
let durationBinOptions = ["1m","10m","1h","6h","12h"];
|
let durationBinOptions = ["1m","10m","1h","6h","12h"];
|
||||||
let metricBinOptions = [10, 20, 50, 100];
|
let metricBinOptions = [10, 20, 50, 100];
|
||||||
|
|
||||||
$: metricsInHistograms = selectedCluster
|
$: selectedHistograms = selectedCluster
|
||||||
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []
|
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
|
||||||
: ccconfig.user_view_histogramMetrics || [];
|
: ccconfig['user_view_histogramMetrics'] || [];
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
$: stats = queryStore({
|
$: stats = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query ($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) {
|
query ($jobFilters: [JobFilter!]!, $selectedHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) {
|
||||||
jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) {
|
jobsStatistics(filter: $jobFilters, metrics: $selectedHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) {
|
||||||
totalJobs
|
totalJobs
|
||||||
shortJobs
|
shortJobs
|
||||||
totalWalltime
|
totalWalltime
|
||||||
@ -104,7 +104,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: { jobFilters, metricsInHistograms, numDurationBins, numMetricBins },
|
variables: { jobFilters, selectedHistograms, numDurationBins, numMetricBins },
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => filterComponent.updateFilters());
|
onMount(() => filterComponent.updateFilters());
|
||||||
@ -290,7 +290,7 @@
|
|||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{#if metricsInHistograms?.length > 0}
|
{#if selectedHistograms?.length > 0}
|
||||||
{#if $stats.error}
|
{#if $stats.error}
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
@ -357,6 +357,6 @@
|
|||||||
|
|
||||||
<HistogramSelection
|
<HistogramSelection
|
||||||
bind:cluster={selectedCluster}
|
bind:cluster={selectedCluster}
|
||||||
bind:metricsInHistograms
|
bind:selectedHistograms
|
||||||
bind:isOpen={isHistogramSelectionOpen}
|
bind:isOpen={isHistogramSelectionOpen}
|
||||||
/>
|
/>
|
||||||
|
@ -43,26 +43,31 @@
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#if $initialized}
|
{#if $initialized}
|
||||||
<h4>Cluster</h4>
|
<h4>Cluster</h4>
|
||||||
<ListGroup>
|
{#if disableClusterSelection}
|
||||||
<ListGroupItem
|
<Button color="info" class="w-100 mb-2" disabled><b>Info: Cluster Selection Disabled in This View</b></Button>
|
||||||
disabled={disableClusterSelection}
|
<Button outline color="primary" class="w-100 mb-2" disabled><b>Selected Cluster: {cluster}</b></Button>
|
||||||
active={pendingCluster == null}
|
{:else}
|
||||||
on:click={() => ((pendingCluster = null), (pendingPartition = null))}
|
<ListGroup>
|
||||||
>
|
|
||||||
Any Cluster
|
|
||||||
</ListGroupItem>
|
|
||||||
{#each clusters as cluster}
|
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
disabled={disableClusterSelection}
|
disabled={disableClusterSelection}
|
||||||
active={pendingCluster == cluster.name}
|
active={pendingCluster == null}
|
||||||
on:click={() => (
|
on:click={() => ((pendingCluster = null), (pendingPartition = null))}
|
||||||
(pendingCluster = cluster.name), (pendingPartition = null)
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{cluster.name}
|
Any Cluster
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{/each}
|
{#each clusters as cluster}
|
||||||
</ListGroup>
|
<ListGroupItem
|
||||||
|
disabled={disableClusterSelection}
|
||||||
|
active={pendingCluster == cluster.name}
|
||||||
|
on:click={() => (
|
||||||
|
(pendingCluster = cluster.name), (pendingPartition = null)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cluster.name}
|
||||||
|
</ListGroupItem>
|
||||||
|
{/each}
|
||||||
|
</ListGroup>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if $initialized && pendingCluster != null}
|
{#if $initialized && pendingCluster != null}
|
||||||
<br />
|
<br />
|
||||||
|
@ -179,7 +179,7 @@
|
|||||||
function render(plotData) {
|
function render(plotData) {
|
||||||
if (plotData) {
|
if (plotData) {
|
||||||
const opts = {
|
const opts = {
|
||||||
title: "",
|
title: "CPU Roofline Diagram",
|
||||||
mode: 2,
|
mode: 2,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `cluster String`: Currently selected cluster
|
- `cluster String`: Currently selected cluster
|
||||||
- `metricsInHistograms [String]`: The currently selected metrics to display as histogram
|
- `selectedHistograms [String]`: The currently selected metrics to display as histogram
|
||||||
- ìsOpen Bool`: Is selection opened
|
- ìsOpen Bool`: Is selection opened
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@ -21,22 +21,27 @@
|
|||||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
|
|
||||||
export let cluster;
|
export let cluster;
|
||||||
export let metricsInHistograms;
|
export let selectedHistograms;
|
||||||
export let isOpen;
|
export let isOpen;
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const initialized = getContext("initialized");
|
const initialized = getContext("initialized");
|
||||||
|
|
||||||
let availableMetrics = []
|
function loadHistoMetrics(isInitialized, thisCluster) {
|
||||||
|
if (!isInitialized) return [];
|
||||||
|
|
||||||
function loadHistoMetrics(isInitialized) {
|
if (!thisCluster) {
|
||||||
if (!isInitialized) return;
|
return getContext("globalMetrics")
|
||||||
const rawAvailableMetrics = getContext("globalMetrics").filter((gm) => gm?.footprint).map((fgm) => { return fgm.name })
|
.filter((gm) => gm?.footprint)
|
||||||
availableMetrics = [...rawAvailableMetrics]
|
.map((fgm) => { return fgm.name })
|
||||||
|
} else {
|
||||||
|
return getContext("globalMetrics")
|
||||||
|
.filter((gm) => gm?.availability.find((av) => av.cluster == thisCluster))
|
||||||
|
.filter((agm) => agm?.footprint)
|
||||||
|
.map((afgm) => { return afgm.name })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pendingMetrics = [...metricsInHistograms]; // Copy
|
|
||||||
|
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
@ -61,17 +66,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeAndApply() {
|
function closeAndApply() {
|
||||||
metricsInHistograms = [...pendingMetrics]; // Set for parent
|
|
||||||
isOpen = !isOpen;
|
isOpen = !isOpen;
|
||||||
updateConfiguration({
|
updateConfiguration({
|
||||||
name: cluster
|
name: cluster
|
||||||
? `user_view_histogramMetrics:${cluster}`
|
? `user_view_histogramMetrics:${cluster}`
|
||||||
: "user_view_histogramMetrics",
|
: "user_view_histogramMetrics",
|
||||||
value: metricsInHistograms,
|
value: selectedHistograms,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: loadHistoMetrics($initialized);
|
$: availableMetrics = loadHistoMetrics($initialized, cluster);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -81,7 +85,7 @@
|
|||||||
<ListGroup>
|
<ListGroup>
|
||||||
{#each availableMetrics as metric (metric)}
|
{#each availableMetrics as metric (metric)}
|
||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
<input type="checkbox" bind:group={pendingMetrics} value={metric} />
|
<input type="checkbox" bind:group={selectedHistograms} value={metric} />
|
||||||
{metric}
|
{metric}
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupText,
|
InputGroupText,
|
||||||
Icon,
|
Icon,
|
||||||
|
Row,
|
||||||
|
Col
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { maxScope } from "../generic/utils.js";
|
import { maxScope } from "../generic/utils.js";
|
||||||
import StatsTableEntry from "./StatsTableEntry.svelte";
|
import StatsTableEntry from "./StatsTableEntry.svelte";
|
||||||
@ -26,7 +28,7 @@
|
|||||||
export let job;
|
export let job;
|
||||||
export let jobMetrics;
|
export let jobMetrics;
|
||||||
|
|
||||||
const allMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort()
|
const sortedJobMetrics = [...new Set(jobMetrics.map((m) => m.name))].sort()
|
||||||
const scopesForMetric = (metric) =>
|
const scopesForMetric = (metric) =>
|
||||||
jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope);
|
jobMetrics.filter((jm) => jm.name == metric).map((jm) => jm.scope);
|
||||||
|
|
||||||
@ -34,11 +36,12 @@
|
|||||||
selectedScopes = {},
|
selectedScopes = {},
|
||||||
sorting = {},
|
sorting = {},
|
||||||
isMetricSelectionOpen = false,
|
isMetricSelectionOpen = false,
|
||||||
|
availableMetrics = new Set(),
|
||||||
selectedMetrics =
|
selectedMetrics =
|
||||||
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
|
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
|
||||||
getContext("cc-config")["job_view_nodestats_selectedMetrics"];
|
getContext("cc-config")["job_view_nodestats_selectedMetrics"];
|
||||||
|
|
||||||
for (let metric of allMetrics) {
|
for (let metric of sortedJobMetrics) {
|
||||||
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
|
// Not Exclusive or Multi-Node: get maxScope directly (mostly: node)
|
||||||
// -> Else: Load smallest available granularity as default as per availability
|
// -> Else: Load smallest available granularity as default as per availability
|
||||||
const availableScopes = scopesForMetric(metric);
|
const availableScopes = scopesForMetric(metric);
|
||||||
@ -95,15 +98,19 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col class="m-2">
|
||||||
|
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="w-auto px-2" color="primary">
|
||||||
|
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<hr class="mb-1 mt-1"/>
|
||||||
<Table class="mb-0">
|
<Table class="mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<!-- Header Row 1: Selectors -->
|
<!-- Header Row 1: Selectors -->
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th/>
|
||||||
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="w-100 px-2" color="primary">
|
|
||||||
Select Metrics
|
|
||||||
</Button>
|
|
||||||
</th>
|
|
||||||
{#each selectedMetrics as metric}
|
{#each selectedMetrics as metric}
|
||||||
<!-- To Match Row-2 Header Field Count-->
|
<!-- To Match Row-2 Header Field Count-->
|
||||||
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
|
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
|
||||||
@ -163,7 +170,7 @@
|
|||||||
<MetricSelection
|
<MetricSelection
|
||||||
cluster={job.cluster}
|
cluster={job.cluster}
|
||||||
configName="job_view_nodestats_selectedMetrics"
|
configName="job_view_nodestats_selectedMetrics"
|
||||||
allMetrics={new Set(allMetrics)}
|
bind:allMetrics={availableMetrics}
|
||||||
bind:metrics={selectedMetrics}
|
bind:metrics={selectedMetrics}
|
||||||
bind:isOpen={isMetricSelectionOpen}
|
bind:isOpen={isMetricSelectionOpen}
|
||||||
/>
|
/>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user