mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-08-02 09:20:36 +02:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9974a851e8 | ||
6c0bfc6c35 | |||
|
41bbd203cc | ||
|
4344c26bef | ||
|
e1c1c06fb2 | ||
|
70e63764ff | ||
|
d10f3e3af6 | ||
|
a4397d5447 | ||
|
320c87a1db | ||
|
8d1228c9e8 | ||
|
420bec7c46 | ||
|
ba1658beac | ||
|
575753038b | ||
|
061c9f0979 | ||
|
b48d1b8ad6 | ||
dff7aeefb8 | |||
54f7980162 | |||
|
597bccc080 | ||
|
72557fd0bf | ||
|
0b2f2214f9 | ||
|
ef51e69ffb | ||
|
c9eb40f455 | ||
|
b66750339d | ||
|
136460567c | ||
|
f80123c85d | ||
|
a22340196f | ||
|
cbaeffde2c | ||
b67f5436f8 | |||
b637ddeb28 |
2
Makefile
2
Makefile
@@ -2,7 +2,7 @@ TARGET = ./cc-backend
|
|||||||
VAR = ./var
|
VAR = ./var
|
||||||
CFG = config.json .env
|
CFG = config.json .env
|
||||||
FRONTEND = ./web/frontend
|
FRONTEND = ./web/frontend
|
||||||
VERSION = 1.3.0
|
VERSION = 1.3.1
|
||||||
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
|
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
|
||||||
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
|
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
|
||||||
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'
|
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
# `cc-backend` version 1.3.0
|
# `cc-backend` version 1.3.1
|
||||||
|
|
||||||
Supports job archive version 1 and database version 7.
|
Supports job archive version 1 and database version 7.
|
||||||
|
|
||||||
This is a minor release of `cc-backend`, the API backend and frontend
|
This is a bugfix release of `cc-backend`, the API backend and frontend
|
||||||
implementation of ClusterCockpit.
|
implementation of ClusterCockpit.
|
||||||
For release specific notes visit the [ClusterCockpit Documentation](https://clusterockpit.org/docs/release/).
|
For release specific notes visit the [ClusterCockpit Documentation](https://clusterockpit.org/docs/release/).
|
||||||
|
|
||||||
## Breaking changes
|
## Breaking changes
|
||||||
|
|
||||||
* This release fixes bugs in the MySQL/MariaDB database schema. For this reason
|
None
|
||||||
you have to migrate your database using the `-migrate-db` switch.
|
|
||||||
|
@@ -76,6 +76,9 @@ const configString = `
|
|||||||
"kind": "file",
|
"kind": "file",
|
||||||
"path": "./var/job-archive"
|
"path": "./var/job-archive"
|
||||||
},
|
},
|
||||||
|
"jwts": {
|
||||||
|
"max-age": "2000h"
|
||||||
|
},
|
||||||
"clusters": [
|
"clusters": [
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "name",
|
||||||
@@ -115,15 +118,15 @@ func initEnv() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile("config.json", []byte(configString), 0666); err != nil {
|
if err := os.WriteFile("config.json", []byte(configString), 0o666); err != nil {
|
||||||
log.Fatalf("Writing config.json failed: %s", err.Error())
|
log.Fatalf("Writing config.json failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(".env", []byte(envString), 0666); err != nil {
|
if err := os.WriteFile(".env", []byte(envString), 0o666); err != nil {
|
||||||
log.Fatalf("Writing .env failed: %s", err.Error())
|
log.Fatalf("Writing .env failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Mkdir("var", 0777); err != nil {
|
if err := os.Mkdir("var", 0o777); err != nil {
|
||||||
log.Fatalf("Mkdir var failed: %s", err.Error())
|
log.Fatalf("Mkdir var failed: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -243,14 +243,21 @@ func (r *queryResolver) Jobs(ctx context.Context, filter []*model.JobFilter, pag
|
|||||||
|
|
||||||
if !config.Keys.UiDefaults["job_list_usePaging"].(bool) {
|
if !config.Keys.UiDefaults["job_list_usePaging"].(bool) {
|
||||||
hasNextPage := false
|
hasNextPage := false
|
||||||
page.Page += 1
|
// page.Page += 1 : Simple, but expensive
|
||||||
|
// Example Page 4 @ 10 IpP : Does item 41 exist?
|
||||||
|
// Minimal Page 41 @ 1 IpP : If len(result) is 1, Page 5 @ 10 IpP exists.
|
||||||
|
nextPage := &model.PageRequest{
|
||||||
|
ItemsPerPage: 1,
|
||||||
|
Page: ((page.Page * page.ItemsPerPage) + 1),
|
||||||
|
}
|
||||||
|
|
||||||
nextJobs, err := r.Repo.QueryJobs(ctx, filter, page, order)
|
nextJobs, err := r.Repo.QueryJobs(ctx, filter, nextPage, order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error while querying next jobs")
|
log.Warn("Error while querying next jobs")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(nextJobs) > 0 {
|
|
||||||
|
if len(nextJobs) == 1 {
|
||||||
hasNextPage = true
|
hasNextPage = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -109,8 +109,8 @@ func LoadData(job *schema.Job,
|
|||||||
jd, err = repo.LoadData(job, metrics, scopes, ctx)
|
jd, err = repo.LoadData(job, metrics, scopes, ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(jd) != 0 {
|
if len(jd) != 0 {
|
||||||
log.Errorf("partial error: %s", err.Error())
|
log.Warnf("partial error: %s", err.Error())
|
||||||
return err, 0, 0
|
// return err, 0, 0 // Reactivating will block archiving on one partial error
|
||||||
} else {
|
} else {
|
||||||
log.Error("Error while loading job data from metric repository")
|
log.Error("Error while loading job data from metric repository")
|
||||||
return err, 0, 0
|
return err, 0, 0
|
||||||
|
@@ -520,7 +520,7 @@ func (r *JobRepository) archivingWorker() {
|
|||||||
// not using meta data, called to load JobMeta into Cache?
|
// not using meta data, called to load JobMeta into Cache?
|
||||||
// will fail if job meta not in repository
|
// will fail if job meta not in repository
|
||||||
if _, err := r.FetchMetadata(job); err != nil {
|
if _, err := r.FetchMetadata(job); err != nil {
|
||||||
log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error())
|
log.Errorf("archiving job (dbid: %d) failed at check metadata step: %s", job.ID, err.Error())
|
||||||
r.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed)
|
r.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -529,14 +529,14 @@ func (r *JobRepository) archivingWorker() {
|
|||||||
// TODO: Maybe use context with cancel/timeout here
|
// TODO: Maybe use context with cancel/timeout here
|
||||||
jobMeta, err := metricdata.ArchiveJob(job, context.Background())
|
jobMeta, err := metricdata.ArchiveJob(job, context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error())
|
log.Errorf("archiving job (dbid: %d) failed at archiving job step: %s", job.ID, err.Error())
|
||||||
r.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed)
|
r.UpdateMonitoringStatus(job.ID, schema.MonitoringStatusArchivingFailed)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the jobs database entry one last time:
|
// Update the jobs database entry one last time:
|
||||||
if err := r.MarkArchived(job.ID, schema.MonitoringStatusArchivingSuccessful, jobMeta.Statistics); err != nil {
|
if err := r.MarkArchived(job.ID, schema.MonitoringStatusArchivingSuccessful, jobMeta.Statistics); err != nil {
|
||||||
log.Errorf("archiving job (dbid: %d) failed: %s", job.ID, err.Error())
|
log.Errorf("archiving job (dbid: %d) failed at marking archived step: %s", job.ID, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Debugf("archiving job %d took %s", job.JobID, time.Since(start))
|
log.Debugf("archiving job %d took %s", job.JobID, time.Since(start))
|
||||||
|
@@ -135,7 +135,7 @@ func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.Select
|
|||||||
query = buildStringCondition("job.project", filter.Project, query)
|
query = buildStringCondition("job.project", filter.Project, query)
|
||||||
}
|
}
|
||||||
if filter.JobName != nil {
|
if filter.JobName != nil {
|
||||||
query = buildStringCondition("job.meta_data", filter.JobName, query)
|
query = buildMetaJsonCondition("jobName", filter.JobName, query)
|
||||||
}
|
}
|
||||||
if filter.Cluster != nil {
|
if filter.Cluster != nil {
|
||||||
query = buildStringCondition("job.cluster", filter.Cluster, query)
|
query = buildStringCondition("job.cluster", filter.Cluster, query)
|
||||||
@@ -235,6 +235,28 @@ func buildStringCondition(field string, cond *model.StringInput, query sq.Select
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildMetaJsonCondition(jsonField string, cond *model.StringInput, query sq.SelectBuilder) sq.SelectBuilder {
|
||||||
|
// Verify and Search Only in Valid Jsons
|
||||||
|
query = query.Where("JSON_VALID(meta_data)")
|
||||||
|
// add "AND" Sql query Block for field match
|
||||||
|
if cond.Eq != nil {
|
||||||
|
return query.Where("JSON_EXTRACT(meta_data, \"$."+jsonField+"\") = ?", *cond.Eq)
|
||||||
|
}
|
||||||
|
if cond.Neq != nil {
|
||||||
|
return query.Where("JSON_EXTRACT(meta_data, \"$."+jsonField+"\") != ?", *cond.Neq)
|
||||||
|
}
|
||||||
|
if cond.StartsWith != nil {
|
||||||
|
return query.Where("JSON_EXTRACT(meta_data, \"$."+jsonField+"\") LIKE ?", fmt.Sprint(*cond.StartsWith, "%"))
|
||||||
|
}
|
||||||
|
if cond.EndsWith != nil {
|
||||||
|
return query.Where("JSON_EXTRACT(meta_data, \"$."+jsonField+"\") LIKE ?", fmt.Sprint("%", *cond.EndsWith))
|
||||||
|
}
|
||||||
|
if cond.Contains != nil {
|
||||||
|
return query.Where("JSON_EXTRACT(meta_data, \"$."+jsonField+"\") LIKE ?", fmt.Sprint("%", *cond.Contains, "%"))
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
||||||
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||||
|
|
||||||
|
@@ -24,9 +24,9 @@ var (
|
|||||||
type UserCfgRepo struct {
|
type UserCfgRepo struct {
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
Lookup *sqlx.Stmt
|
Lookup *sqlx.Stmt
|
||||||
lock sync.RWMutex
|
|
||||||
uiDefaults map[string]interface{}
|
uiDefaults map[string]interface{}
|
||||||
cache *lrucache.Cache
|
cache *lrucache.Cache
|
||||||
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserCfgRepo() *UserCfgRepo {
|
func GetUserCfgRepo() *UserCfgRepo {
|
||||||
@@ -112,8 +112,8 @@ func (uCfg *UserCfgRepo) GetUIConfig(user *schema.User) (map[string]interface{},
|
|||||||
// configuration.
|
// configuration.
|
||||||
func (uCfg *UserCfgRepo) UpdateConfig(
|
func (uCfg *UserCfgRepo) UpdateConfig(
|
||||||
key, value string,
|
key, value string,
|
||||||
user *schema.User) error {
|
user *schema.User,
|
||||||
|
) error {
|
||||||
if user == nil {
|
if user == nil {
|
||||||
var val interface{}
|
var val interface{}
|
||||||
if err := json.Unmarshal([]byte(value), &val); err != nil {
|
if err := json.Unmarshal([]byte(value), &val); err != nil {
|
||||||
|
@@ -302,11 +302,19 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
|
|||||||
case "jobId":
|
case "jobId":
|
||||||
http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
|
http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
|
||||||
case "jobName":
|
case "jobName":
|
||||||
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
|
// Add Last 30 Days to migitate timeouts
|
||||||
|
untilTime := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
|
||||||
|
|
||||||
|
http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
|
||||||
case "projectId":
|
case "projectId":
|
||||||
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
|
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
|
||||||
case "arrayJobId":
|
case "arrayJobId":
|
||||||
http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
|
// Add Last 30 Days to migitate timeouts
|
||||||
|
untilTime := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
|
||||||
|
|
||||||
|
http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
|
||||||
case "username":
|
case "username":
|
||||||
if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
|
if user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
|
||||||
http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)
|
http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)
|
||||||
@@ -339,7 +347,11 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
|
|||||||
} else if project != "" {
|
} else if project != "" {
|
||||||
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(project), http.StatusFound) // projectId (equal)
|
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(project), http.StatusFound) // projectId (equal)
|
||||||
} else if jobname != "" {
|
} else if jobname != "" {
|
||||||
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(jobname), http.StatusFound) // JobName (contains)
|
// Add Last 30 Days to migitate timeouts
|
||||||
|
untilTime := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
fromTime := strconv.FormatInt((time.Now().Unix() - int64(30*24*3600)), 10)
|
||||||
|
|
||||||
|
http.Redirect(rw, r, "/monitoring/jobs/?startTime="+fromTime+"-"+untilTime+"&jobName="+url.QueryEscape(jobname), http.StatusFound) // 30D Fitler + JobName (contains)
|
||||||
} else {
|
} else {
|
||||||
web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo})
|
web.RenderTemplate(rw, "message.tmpl", &web.Page{Title: "Info", MsgType: "alert-info", Message: "Search without result", User: *user, Roles: availableRoles, Build: buildInfo})
|
||||||
}
|
}
|
||||||
|
@@ -165,10 +165,11 @@
|
|||||||
.find((c) => c.name == job.cluster)
|
.find((c) => c.name == job.cluster)
|
||||||
.metricConfig.map((mc) => mc.name);
|
.metricConfig.map((mc) => mc.name);
|
||||||
|
|
||||||
// Metric not found in JobMetrics && Metric not explicitly disabled: Was expected, but is Missing
|
// Metric not found in JobMetrics && Metric not explicitly disabled in config or deselected: Was expected, but is Missing
|
||||||
missingMetrics = metricNames.filter(
|
missingMetrics = metricNames.filter(
|
||||||
(metric) =>
|
(metric) =>
|
||||||
!metrics.some((jm) => jm.name == metric) &&
|
!metrics.some((jm) => jm.name == metric) &&
|
||||||
|
selectedMetrics.includes(metric) &&
|
||||||
!checkMetricDisabled(
|
!checkMetricDisabled(
|
||||||
metric,
|
metric,
|
||||||
$initq.data.job.cluster,
|
$initq.data.job.cluster,
|
||||||
@@ -306,9 +307,6 @@
|
|||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
<!-- <Col xs="auto">
|
|
||||||
<Zoom timeseriesPlots={plots} />
|
|
||||||
</Col> -->
|
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col>
|
||||||
@@ -341,7 +339,6 @@
|
|||||||
scopes={item.data.map((x) => x.scope)}
|
scopes={item.data.map((x) => x.scope)}
|
||||||
{width}
|
{width}
|
||||||
isShared={$initq.data.job.exclusive != 1}
|
isShared={$initq.data.job.exclusive != 1}
|
||||||
resources={$initq.data.job.resources}
|
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning"
|
<Card body color="warning"
|
||||||
@@ -361,7 +358,7 @@
|
|||||||
<div style="margin: 10px;">
|
<div style="margin: 10px;">
|
||||||
<Card color="warning">
|
<Card color="warning">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Missing Metrics/Reseources</CardTitle>
|
<CardTitle>Missing Metrics/Resources</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{#if missingMetrics.length > 0}
|
{#if missingMetrics.length > 0}
|
||||||
|
@@ -101,7 +101,10 @@
|
|||||||
// Calculate Avg from jobMetrics
|
// Calculate Avg from jobMetrics
|
||||||
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === "node");
|
const jm = jobMetrics.find((jm) => jm.name === fm && jm.scope === "node");
|
||||||
if (jm?.metric?.statisticsSeries) {
|
if (jm?.metric?.statisticsSeries) {
|
||||||
mv = round(mean(jm.metric.statisticsSeries.mean), 2);
|
const noNan = jm.metric.statisticsSeries.mean.filter(function (val) {
|
||||||
|
return val != null;
|
||||||
|
});
|
||||||
|
mv = round(mean(noNan), 2);
|
||||||
} else if (jm?.metric?.series?.length > 1) {
|
} else if (jm?.metric?.series?.length > 1) {
|
||||||
const avgs = jm.metric.series.map((jms) => jms.statistics.avg);
|
const avgs = jm.metric.series.map((jms) => jms.statistics.avg);
|
||||||
mv = round(mean(avgs), 2);
|
mv = round(mean(avgs), 2);
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
import Refresher from "./joblist/Refresher.svelte";
|
import Refresher from "./joblist/Refresher.svelte";
|
||||||
import Sorting from "./joblist/SortSelection.svelte";
|
import Sorting from "./joblist/SortSelection.svelte";
|
||||||
import MetricSelection from "./MetricSelection.svelte";
|
import MetricSelection from "./MetricSelection.svelte";
|
||||||
import UserOrProject from "./filters/UserOrProject.svelte";
|
import TextFilter from "./filters/TextFilter.svelte";
|
||||||
|
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||||
: !!ccconfig.plot_list_showFootprint;
|
: !!ccconfig.plot_list_showFootprint;
|
||||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
||||||
|
let presetProject = filterPresets?.project ? filterPresets.project : ""
|
||||||
|
|
||||||
// The filterPresets are handled by the Filters component,
|
// The filterPresets are handled by the Filters component,
|
||||||
// so we need to wait for it to be ready before we can start a query.
|
// so we need to wait for it to be ready before we can start a query.
|
||||||
@@ -86,7 +87,8 @@
|
|||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs="3" style="margin-left: auto;">
|
<Col xs="3" style="margin-left: auto;">
|
||||||
<UserOrProject
|
<TextFilter
|
||||||
|
{presetProject}
|
||||||
bind:authlevel
|
bind:authlevel
|
||||||
bind:roles
|
bind:roles
|
||||||
on:update={({ detail }) => filterComponent.update(detail)}
|
on:update={({ detail }) => filterComponent.update(detail)}
|
||||||
|
@@ -33,8 +33,17 @@
|
|||||||
error = null;
|
error = null;
|
||||||
let selectedScope = minScope(scopes);
|
let selectedScope = minScope(scopes);
|
||||||
|
|
||||||
|
let statsPattern = /(.*)-stats$/
|
||||||
|
let statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null)
|
||||||
|
let selectedScopeIndex
|
||||||
|
|
||||||
$: availableScopes = scopes;
|
$: availableScopes = scopes;
|
||||||
$: selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
|
$: patternMatches = statsPattern.exec(selectedScope)
|
||||||
|
$: if (!patternMatches) {
|
||||||
|
selectedScopeIndex = scopes.findIndex((s) => s == selectedScope);
|
||||||
|
} else {
|
||||||
|
selectedScopeIndex = scopes.findIndex((s) => s == patternMatches[1]);
|
||||||
|
}
|
||||||
$: data = rawData[selectedScopeIndex];
|
$: data = rawData[selectedScopeIndex];
|
||||||
$: series = data?.series.filter(
|
$: series = data?.series.filter(
|
||||||
(series) => selectedHost == null || series.hostname == selectedHost,
|
(series) => selectedHost == null || series.hostname == selectedHost,
|
||||||
@@ -62,6 +71,7 @@
|
|||||||
if (jm.scope != "node") {
|
if (jm.scope != "node") {
|
||||||
scopes = [...scopes, jm.scope];
|
scopes = [...scopes, jm.scope];
|
||||||
rawData.push(jm.metric);
|
rawData.push(jm.metric);
|
||||||
|
statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null)
|
||||||
selectedScope = jm.scope;
|
selectedScope = jm.scope;
|
||||||
selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
|
selectedScopeIndex = scopes.findIndex((s) => s == jm.scope);
|
||||||
dispatch("more-loaded", jm);
|
dispatch("more-loaded", jm);
|
||||||
@@ -79,15 +89,18 @@
|
|||||||
: "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
|
: "") + (metricConfig?.unit?.base ? metricConfig.unit.base : "")})
|
||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
<select class="form-select" bind:value={selectedScope}>
|
<select class="form-select" bind:value={selectedScope}>
|
||||||
{#each availableScopes as scope}
|
{#each availableScopes as scope, index}
|
||||||
<option value={scope}>{scope}</option>
|
<option value={scope}>{scope}</option>
|
||||||
|
{#if statsSeries[index]}
|
||||||
|
<option value={scope + '-stats'}>stats series ({scope})</option>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if availableScopes.length == 1 && metricConfig?.scope != "node"}
|
{#if availableScopes.length == 1 && metricConfig?.scope != "node"}
|
||||||
<option value={"load-more"}>Load more...</option>
|
<option value={"load-more"}>Load more...</option>
|
||||||
{/if}
|
{/if}
|
||||||
</select>
|
</select>
|
||||||
{#if job.resources.length > 1}
|
{#if job.resources.length > 1}
|
||||||
<select class="form-select" bind:value={selectedHost}>
|
<select class="form-select" bind:value={selectedHost} disabled={patternMatches}>
|
||||||
<option value={null}>All Hosts</option>
|
<option value={null}>All Hosts</option>
|
||||||
{#each job.resources as { hostname }}
|
{#each job.resources as { hostname }}
|
||||||
<option value={hostname}>{hostname}</option>
|
<option value={hostname}>{hostname}</option>
|
||||||
@@ -100,7 +113,7 @@
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if error != null}
|
{:else if error != null}
|
||||||
<Card body color="danger">{error.message}</Card>
|
<Card body color="danger">{error.message}</Card>
|
||||||
{:else if series != null}
|
{:else if series != null && !patternMatches}
|
||||||
<Timeseries
|
<Timeseries
|
||||||
bind:this={plot}
|
bind:this={plot}
|
||||||
{width}
|
{width}
|
||||||
@@ -114,5 +127,21 @@
|
|||||||
{isShared}
|
{isShared}
|
||||||
resources={job.resources}
|
resources={job.resources}
|
||||||
/>
|
/>
|
||||||
|
{:else if statsSeries[selectedScopeIndex] != null && patternMatches}
|
||||||
|
<Timeseries
|
||||||
|
bind:this={plot}
|
||||||
|
{width}
|
||||||
|
height={300}
|
||||||
|
{cluster}
|
||||||
|
{subCluster}
|
||||||
|
timestep={data.timestep}
|
||||||
|
scope={selectedScope}
|
||||||
|
metric={metricName}
|
||||||
|
{series}
|
||||||
|
{isShared}
|
||||||
|
resources={job.resources}
|
||||||
|
statisticsSeries={statsSeries[selectedScopeIndex]}
|
||||||
|
useStatsSeries={!!statsSeries[selectedScopeIndex]}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
|
@@ -24,7 +24,7 @@
|
|||||||
export let configName;
|
export let configName;
|
||||||
export let allMetrics = null;
|
export let allMetrics = null;
|
||||||
export let cluster = null;
|
export let cluster = null;
|
||||||
export let showFootprint;
|
export let showFootprint = false;
|
||||||
export let view = "job";
|
export let view = "job";
|
||||||
|
|
||||||
const clusters = getContext("clusters"),
|
const clusters = getContext("clusters"),
|
||||||
|
@@ -315,20 +315,11 @@
|
|||||||
|
|
||||||
<!-- Loading indicator & Refresh -->
|
<!-- Loading indicator & Refresh -->
|
||||||
|
|
||||||
<Row cols={3}>
|
<Row cols={{ lg: 3, md: 3, sm: 1 }}>
|
||||||
<Col xs="auto" style="align-self: flex-end;">
|
<Col style="">
|
||||||
<h4 class="mb-0">Current utilization of cluster "{cluster}"</h4>
|
<h4 class="mb-0">Current utilization of cluster "{cluster}"</h4>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto" style="margin-left: 0.25rem;">
|
<Col class="mt-2 mt-md-0 text-md-end">
|
||||||
{#if $initq.fetching || $mainQuery.fetching}
|
|
||||||
<Spinner />
|
|
||||||
{:else if $initq.error}
|
|
||||||
<Card body color="danger">{$initq.error.message}</Card>
|
|
||||||
{:else}
|
|
||||||
<!-- ... -->
|
|
||||||
{/if}
|
|
||||||
</Col>
|
|
||||||
<Col xs="auto" style="margin-left: auto;">
|
|
||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -337,7 +328,7 @@
|
|||||||
<Icon name="bar-chart-line" /> Select Histograms
|
<Icon name="bar-chart-line" /> Select Histograms
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto" style="margin-left: 0.25rem;">
|
<Col class="mt-2 mt-md-0">
|
||||||
<Refresher
|
<Refresher
|
||||||
initially={120}
|
initially={120}
|
||||||
on:reload={() => {
|
on:reload={() => {
|
||||||
@@ -347,6 +338,17 @@
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row cols={1} class="text-center mt-3">
|
||||||
|
<Col>
|
||||||
|
{#if $initq.fetching || $mainQuery.fetching}
|
||||||
|
<Spinner />
|
||||||
|
{:else if $initq.error}
|
||||||
|
<Card body color="danger">{$initq.error.message}</Card>
|
||||||
|
{:else}
|
||||||
|
<!-- ... -->
|
||||||
|
{/if}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
{#if $mainQuery.error}
|
{#if $mainQuery.error}
|
||||||
<Row cols={1}>
|
<Row cols={1}>
|
||||||
<Col>
|
<Col>
|
||||||
@@ -361,8 +363,8 @@
|
|||||||
|
|
||||||
{#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 cols={2} class="mb-3 justify-content-center">
|
<Row cols={{ lg: 2, md: 1 , sm: 1}} class="mb-3 justify-content-center">
|
||||||
<Col md="4" class="px-3">
|
<Col class="px-3">
|
||||||
<Card class="h-auto mt-1">
|
<Card class="h-auto mt-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-0">SubCluster "{subCluster.name}"</CardTitle>
|
<CardTitle class="mb-0">SubCluster "{subCluster.name}"</CardTitle>
|
||||||
@@ -433,7 +435,7 @@
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="px-3">
|
<Col class="px-3 mt-2 mt-lg-0">
|
||||||
<div bind:clientWidth={plotWidths[i]}>
|
<div bind:clientWidth={plotWidths[i]}>
|
||||||
{#key $mainQuery.data.nodeMetrics}
|
{#key $mainQuery.data.nodeMetrics}
|
||||||
<Roofline
|
<Roofline
|
||||||
@@ -457,7 +459,7 @@
|
|||||||
|
|
||||||
<!-- Usage Stats as Histograms -->
|
<!-- Usage Stats as Histograms -->
|
||||||
|
|
||||||
<Row cols={4}>
|
<Row cols={{ lg: 4, md: 2, sm: 1 }}>
|
||||||
<Col class="p-2">
|
<Col class="p-2">
|
||||||
<div bind:clientWidth={colWidth1}>
|
<div bind:clientWidth={colWidth1}>
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
@@ -580,7 +582,7 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<hr class="my-2" />
|
<hr class="my-2" />
|
||||||
<Row cols={2}>
|
<Row cols={{ lg: 2, md: 1 }}>
|
||||||
<Col class="p-2">
|
<Col class="p-2">
|
||||||
<div bind:clientWidth={colWidth2}>
|
<div bind:clientWidth={colWidth2}>
|
||||||
{#key $mainQuery.data.stats}
|
{#key $mainQuery.data.stats}
|
||||||
@@ -610,7 +612,7 @@
|
|||||||
{/key}
|
{/key}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row cols={2}>
|
<Row cols={{ lg: 2, md: 1 }}>
|
||||||
<Col class="p-2">
|
<Col class="p-2">
|
||||||
<div bind:clientWidth={colWidth2}>
|
<div bind:clientWidth={colWidth2}>
|
||||||
{#key $mainQuery.data.stats}
|
{#key $mainQuery.data.stats}
|
||||||
@@ -642,7 +644,7 @@
|
|||||||
</Row>
|
</Row>
|
||||||
<hr class="my-2" />
|
<hr class="my-2" />
|
||||||
{#if metricsInHistograms}
|
{#if metricsInHistograms}
|
||||||
<Row>
|
<Row cols={1}>
|
||||||
<Col>
|
<Col>
|
||||||
{#key $mainQuery.data.stats[0].histMetrics}
|
{#key $mainQuery.data.stats[0].histMetrics}
|
||||||
<PlotTable
|
<PlotTable
|
||||||
@@ -650,7 +652,7 @@
|
|||||||
let:width
|
let:width
|
||||||
renderFor="user"
|
renderFor="user"
|
||||||
items={$mainQuery.data.stats[0].histMetrics}
|
items={$mainQuery.data.stats[0].histMetrics}
|
||||||
itemsPerRow={3}
|
itemsPerRow={2}
|
||||||
>
|
>
|
||||||
<Histogram
|
<Histogram
|
||||||
data={convert2uplot(item.data)}
|
data={convert2uplot(item.data)}
|
||||||
|
@@ -12,6 +12,7 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import Filters from "./filters/Filters.svelte";
|
import Filters from "./filters/Filters.svelte";
|
||||||
|
import TextFilter from "./filters/TextFilter.svelte"
|
||||||
import JobList from "./joblist/JobList.svelte";
|
import JobList from "./joblist/JobList.svelte";
|
||||||
import Sorting from "./joblist/SortSelection.svelte";
|
import Sorting from "./joblist/SortSelection.svelte";
|
||||||
import Refresher from "./joblist/Refresher.svelte";
|
import Refresher from "./joblist/Refresher.svelte";
|
||||||
@@ -132,6 +133,11 @@
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs="auto" style="margin-left: auto;">
|
<Col xs="auto" style="margin-left: auto;">
|
||||||
|
<TextFilter
|
||||||
|
on:update={({ detail }) => filterComponent.update(detail)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs="auto">
|
||||||
<Refresher on:reload={() => jobList.refresh()} />
|
<Refresher on:reload={() => jobList.refresh()} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@@ -275,7 +275,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={3} class="p-2 g-2">
|
<Row cols={4} class="p-2 g-2">
|
||||||
<!-- LINE WIDTH -->
|
<!-- LINE WIDTH -->
|
||||||
<Col
|
<Col
|
||||||
><Card class="h-100">
|
><Card class="h-100">
|
||||||
@@ -422,6 +422,60 @@
|
|||||||
</form>
|
</form>
|
||||||
</Card></Col
|
</Card></Col
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- PAGING -->
|
||||||
|
<Col
|
||||||
|
><Card class="h-100">
|
||||||
|
<form
|
||||||
|
id="paging-form"
|
||||||
|
method="post"
|
||||||
|
action="/api/configuration/"
|
||||||
|
class="card-body"
|
||||||
|
on:submit|preventDefault={() =>
|
||||||
|
handleSettingSubmit("#paging-form", "pag")}
|
||||||
|
>
|
||||||
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
|
<CardTitle
|
||||||
|
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||||
|
>
|
||||||
|
<div>Paging Type</div>
|
||||||
|
{#if displayMessage && message.target == "pag"}<div
|
||||||
|
style="margin-left: auto; font-size: 0.9em;"
|
||||||
|
>
|
||||||
|
<code style="color: {message.color};" out:fade
|
||||||
|
>Update: {message.msg}</code
|
||||||
|
>
|
||||||
|
</div>{/if}
|
||||||
|
</CardTitle>
|
||||||
|
<input type="hidden" name="key" value="job_list_usePaging" />
|
||||||
|
<div class="mb-3">
|
||||||
|
<div>
|
||||||
|
{#if config.job_list_usePaging}
|
||||||
|
<input type="radio" id="true" name="value" value="true" checked />
|
||||||
|
{:else}
|
||||||
|
<input type="radio" id="true" name="value" value="true" />
|
||||||
|
{/if}
|
||||||
|
<label for="true">Paging with selectable count of jobs.</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{#if config.job_list_usePaging}
|
||||||
|
<input type="radio" id="false" name="value" value="false" />
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="false"
|
||||||
|
name="value"
|
||||||
|
value="false"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<label for="false">Continuous scroll iteratively adding 10 jobs.</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button color="primary" type="submit">Submit</Button>
|
||||||
|
</form>
|
||||||
|
</Card></Col
|
||||||
|
>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row cols={1} class="p-2 g-2">
|
<Row cols={1} class="p-2 g-2">
|
||||||
|
@@ -18,9 +18,9 @@
|
|||||||
|
|
||||||
<td>{user.username}</td>
|
<td>{user.username}</td>
|
||||||
<td>{user.name}</td>
|
<td>{user.name}</td>
|
||||||
<td>{user.projects}</td>
|
<td style="max-width: 200px;">{user.projects}</td>
|
||||||
<td>{user.email}</td>
|
<td>{user.email}</td>
|
||||||
<td><code>{user.roles.join(", ")}</code></td>
|
<td><code>{user?.roles ? user.roles.join(", ") : "No Roles"}</code></td>
|
||||||
<td>
|
<td>
|
||||||
{#if !jwt}
|
{#if !jwt}
|
||||||
<Button color="success" on:click={getUserJwt(user.username)}
|
<Button color="success" on:click={getUserJwt(user.username)}
|
||||||
|
@@ -193,7 +193,8 @@
|
|||||||
opts.push(`userMatch=${filters.userMatch}`);
|
opts.push(`userMatch=${filters.userMatch}`);
|
||||||
if (filters.project) opts.push(`project=${filters.project}`);
|
if (filters.project) opts.push(`project=${filters.project}`);
|
||||||
if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
|
if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
|
||||||
if (filters.projectMatch != "contains")
|
if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
|
||||||
|
if (filters.project && filters.projectMatch != "contains")
|
||||||
opts.push(`projectMatch=${filters.projectMatch}`);
|
opts.push(`projectMatch=${filters.projectMatch}`);
|
||||||
|
|
||||||
if (opts.length == 0 && window.location.search.length <= 1) return;
|
if (opts.length == 0 && window.location.search.length <= 1) return;
|
||||||
|
101
web/frontend/src/filters/TextFilter.svelte
Normal file
101
web/frontend/src/filters/TextFilter.svelte
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script>
|
||||||
|
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { scramble, scrambleNames } from "../joblist/JobInfo.svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let presetProject = ""; // If page with this component has project preset, keep preset until reset
|
||||||
|
export let authlevel = null;
|
||||||
|
export let roles = null;
|
||||||
|
let mode = presetProject ? "jobName" : "project";
|
||||||
|
let term = "";
|
||||||
|
let user = "";
|
||||||
|
let project = presetProject ? presetProject : "";
|
||||||
|
let jobName = "";
|
||||||
|
const throttle = 500;
|
||||||
|
|
||||||
|
function modeChanged() {
|
||||||
|
if (mode == "user") {
|
||||||
|
project = presetProject ? presetProject : "";
|
||||||
|
jobName = "";
|
||||||
|
} else if (mode == "project") {
|
||||||
|
user = "";
|
||||||
|
jobName = "";
|
||||||
|
} else {
|
||||||
|
project = presetProject ? presetProject : "";
|
||||||
|
user = "";
|
||||||
|
}
|
||||||
|
termChanged(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeoutId = null;
|
||||||
|
// Compatibility: Handle "user role" and "no role" identically
|
||||||
|
function termChanged(sleep = throttle) {
|
||||||
|
if (roles && authlevel >= roles.manager) {
|
||||||
|
if (mode == "user") user = term;
|
||||||
|
else if (mode == "project") project = term;
|
||||||
|
else jobName = term;
|
||||||
|
|
||||||
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
dispatch("update", {
|
||||||
|
user,
|
||||||
|
project,
|
||||||
|
jobName
|
||||||
|
});
|
||||||
|
}, sleep);
|
||||||
|
} else {
|
||||||
|
if (mode == "project") project = term;
|
||||||
|
else jobName = term;
|
||||||
|
|
||||||
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
dispatch("update", {
|
||||||
|
project,
|
||||||
|
jobName
|
||||||
|
});
|
||||||
|
}, sleep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetProject () {
|
||||||
|
mode = "project"
|
||||||
|
term = ""
|
||||||
|
presetProject = ""
|
||||||
|
project = ""
|
||||||
|
termChanged(0);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<select
|
||||||
|
style="max-width: 175px;"
|
||||||
|
class="form-select"
|
||||||
|
bind:value={mode}
|
||||||
|
on:change={modeChanged}
|
||||||
|
>
|
||||||
|
{#if !presetProject}
|
||||||
|
<option value={"project"}>Search Project</option>
|
||||||
|
{/if}
|
||||||
|
{#if roles && authlevel >= roles.manager}
|
||||||
|
<option value={"user"}>Search User</option>
|
||||||
|
{/if}
|
||||||
|
<option value={"jobName"}>Search Jobname</option>
|
||||||
|
</select>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
bind:value={term}
|
||||||
|
on:change={() => termChanged()}
|
||||||
|
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
||||||
|
placeholder={presetProject ? `Filter ${mode} in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Filter ${mode} ...`}
|
||||||
|
/>
|
||||||
|
{#if presetProject}
|
||||||
|
<Button title="Reset Project" on:click={resetProject}
|
||||||
|
><Icon name="arrow-counterclockwise" /></Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</InputGroup>
|
||||||
|
|
@@ -1,84 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { InputGroup, Input } from "@sveltestrap/sveltestrap";
|
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
export let user = "";
|
|
||||||
export let project = "";
|
|
||||||
export let authlevel;
|
|
||||||
export let roles;
|
|
||||||
let mode = "user",
|
|
||||||
term = "";
|
|
||||||
const throttle = 500;
|
|
||||||
|
|
||||||
function modeChanged() {
|
|
||||||
if (mode == "user") {
|
|
||||||
project = term;
|
|
||||||
term = user;
|
|
||||||
} else {
|
|
||||||
user = term;
|
|
||||||
term = project;
|
|
||||||
}
|
|
||||||
termChanged(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeoutId = null;
|
|
||||||
// Compatibility: Handle "user role" and "no role" identically
|
|
||||||
function termChanged(sleep = throttle) {
|
|
||||||
if (authlevel >= roles.manager) {
|
|
||||||
if (mode == "user") user = term;
|
|
||||||
else project = term;
|
|
||||||
|
|
||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
dispatch("update", {
|
|
||||||
user,
|
|
||||||
project,
|
|
||||||
});
|
|
||||||
}, sleep);
|
|
||||||
} else {
|
|
||||||
project = term;
|
|
||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
dispatch("update", {
|
|
||||||
project,
|
|
||||||
});
|
|
||||||
}, sleep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if authlevel >= roles.manager}
|
|
||||||
<InputGroup>
|
|
||||||
<select
|
|
||||||
style="max-width: 175px;"
|
|
||||||
class="form-select"
|
|
||||||
bind:value={mode}
|
|
||||||
on:change={modeChanged}
|
|
||||||
>
|
|
||||||
<option value={"user"}>Search User</option>
|
|
||||||
<option value={"project"}>Search Project</option>
|
|
||||||
</select>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
bind:value={term}
|
|
||||||
on:change={() => termChanged()}
|
|
||||||
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
|
||||||
placeholder={mode == "user" ? "filter username..." : "filter project..."}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
{:else}
|
|
||||||
<!-- Compatibility: Handle "user role" and "no role" identically-->
|
|
||||||
<InputGroup>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
bind:value={term}
|
|
||||||
on:change={() => termChanged()}
|
|
||||||
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
|
||||||
placeholder="filter project..."
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
{/if}
|
|
@@ -461,7 +461,7 @@
|
|||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: { time: false },
|
x: { time: false },
|
||||||
y: maxY ? { range: [0, maxY * 1.1] } : {},
|
y: maxY ? { min: 0, max: (maxY * 1.1) } : {auto: true}, // Add some space to upper render limit
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
// Display legend until max 12 Y-dataseries
|
// Display legend until max 12 Y-dataseries
|
||||||
|
@@ -298,6 +298,24 @@
|
|||||||
// Reset grid lineWidth
|
// Reset grid lineWidth
|
||||||
u.ctx.lineWidth = 0.15;
|
u.ctx.lineWidth = 0.15;
|
||||||
}
|
}
|
||||||
|
if (renderTime) {
|
||||||
|
// The Color Scale For Time Information
|
||||||
|
const posX = u.valToPos(0.1, "x", true)
|
||||||
|
const posXLimit = u.valToPos(100, "x", true)
|
||||||
|
const posY = u.valToPos(15000.0, "y", true)
|
||||||
|
u.ctx.fillStyle = 'black'
|
||||||
|
u.ctx.fillText('Start', posX, posY)
|
||||||
|
const start = posX + 10
|
||||||
|
for (let x = start; x < posXLimit; x += 10) {
|
||||||
|
let c = (x - start) / (posXLimit - start)
|
||||||
|
u.ctx.fillStyle = getRGB(c)
|
||||||
|
u.ctx.beginPath()
|
||||||
|
u.ctx.arc(x, posY, 3, 0, Math.PI * 2, false)
|
||||||
|
u.ctx.fill()
|
||||||
|
}
|
||||||
|
u.ctx.fillStyle = 'black'
|
||||||
|
u.ctx.fillText('End', posXLimit + 23, posY)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user