Merge branch 'hotfix' of github.com:ClusterCockpit/cc-backend into hotfix

This commit is contained in:
Jan Eitzinger 2024-05-16 11:19:00 +02:00
commit dff7aeefb8
10 changed files with 146 additions and 20 deletions

View File

@ -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
} }

View File

@ -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])")

View File

@ -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 {

View File

@ -306,9 +306,6 @@
</Button> </Button>
{/if} {/if}
</Col> </Col>
<!-- <Col xs="auto">
<Zoom timeseriesPlots={plots} />
</Col> -->
</Row> </Row>
<Row> <Row>
<Col> <Col>
@ -341,7 +338,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 +357,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}

View File

@ -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}

View File

@ -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"),

View File

@ -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">

View File

@ -20,7 +20,7 @@
<td>{user.name}</td> <td>{user.name}</td>
<td>{user.projects}</td> <td>{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)}

View File

@ -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

View File

@ -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)
}
}, },
], ],
}, },