feat: add nodename matcher select to filter, defaults to equal match

- see PR !353
This commit is contained in:
Christoph Kluge 2025-04-08 14:52:07 +02:00
parent b3a1037ade
commit d770292be8
4 changed files with 67 additions and 17 deletions

View File

@ -194,10 +194,12 @@ func (r *JobRepository) FindConcurrentJobs(
queryRunning := query.Where("job.job_state = ?").Where("(job.start_time BETWEEN ? AND ? OR job.start_time < ?)", queryRunning := query.Where("job.job_state = ?").Where("(job.start_time BETWEEN ? AND ? OR job.start_time < ?)",
"running", startTimeTail, stopTimeTail, startTime) "running", startTimeTail, stopTimeTail, startTime)
// Get At Least One Exact Hostname Match from JSON Resources Array in Database
queryRunning = queryRunning.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') = ?)", hostname) queryRunning = queryRunning.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') = ?)", hostname)
query = query.Where("job.job_state != ?").Where("((job.start_time BETWEEN ? AND ?) OR (job.start_time + job.duration) BETWEEN ? AND ? OR (job.start_time < ?) AND (job.start_time + job.duration) > ?)", query = query.Where("job.job_state != ?").Where("((job.start_time BETWEEN ? AND ?) OR (job.start_time + job.duration) BETWEEN ? AND ? OR (job.start_time < ?) AND (job.start_time + job.duration) > ?)",
"running", startTimeTail, stopTimeTail, startTimeFront, stopTimeTail, startTime, stopTime) "running", startTimeTail, stopTimeTail, startTimeFront, stopTimeTail, startTime, stopTime)
// Get At Least One Exact Hostname Match from JSON Resources Array in Database
query = query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') = ?)", hostname) query = query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') = ?)", hostname)
rows, err := query.RunWith(r.stmtCache).Query() rows, err := query.RunWith(r.stmtCache).Query()

View File

@ -67,7 +67,8 @@ func (r *JobRepository) QueryJobs(
rows, err := query.RunWith(r.stmtCache).Query() rows, err := query.RunWith(r.stmtCache).Query()
if err != nil { if err != nil {
log.Errorf("Error while running query: %v", err) queryString, queryVars, _ := query.ToSql()
log.Errorf("Error while running query '%s' %v: %v", queryString, queryVars, err)
return nil, err return nil, err
} }
@ -197,14 +198,7 @@ func BuildWhereClause(filter *model.JobFilter, query sq.SelectBuilder) sq.Select
query = buildIntCondition("job.num_hwthreads", filter.NumHWThreads, query) query = buildIntCondition("job.num_hwthreads", filter.NumHWThreads, query)
} }
if filter.Node != nil { if filter.Node != nil {
log.Infof("Applying node filter: %v", filter.Node) query = buildResourceJsonCondition("hostname", filter.Node, query)
if filter.Node.Eq != nil {
query = query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') = ?)", *filter.Node.Eq)
} else if filter.Node.Contains != nil {
query = query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, '$.hostname') LIKE ?)", "%"+*filter.Node.Contains+"%")
} else {
query = buildStringCondition("job.resources", filter.Node, query)
}
} }
if filter.Energy != nil { if filter.Energy != nil {
query = buildFloatCondition("job.energy", filter.Energy, query) query = buildFloatCondition("job.energy", filter.Energy, query)
@ -306,6 +300,28 @@ func buildMetaJsonCondition(jsonField string, cond *model.StringInput, query sq.
return query return query
} }
func buildResourceJsonCondition(jsonField string, cond *model.StringInput, query sq.SelectBuilder) sq.SelectBuilder {
// Verify and Search Only in Valid Jsons
query = query.Where("JSON_VALID(resources)")
// add "AND" Sql query Block for field match
if cond.Eq != nil {
return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\") = ?)", *cond.Eq)
}
if cond.Neq != nil { // Currently Unused
return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\") != ?)", *cond.Neq)
}
if cond.StartsWith != nil { // Currently Unused
return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\")) LIKE ?)", fmt.Sprint(*cond.StartsWith, "%"))
}
if cond.EndsWith != nil { // Currently Unused
return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\") LIKE ?)", fmt.Sprint("%", *cond.EndsWith))
}
if cond.Contains != nil {
return query.Where("EXISTS (SELECT 1 FROM json_each(job.resources) WHERE json_extract(value, \"$."+jsonField+"\") LIKE ?)", fmt.Sprint("%", *cond.Contains, "%"))
}
return query
}
var ( var (
matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")

View File

@ -53,10 +53,16 @@
{ range: "last30d", rangeLabel: "Last 30 days"} { range: "last30d", rangeLabel: "Last 30 days"}
]; ];
const nodeMatchLabels = {
eq: "",
contains: " Contains",
}
let filters = { let filters = {
projectMatch: filterPresets.projectMatch || "contains", projectMatch: filterPresets.projectMatch || "contains",
userMatch: filterPresets.userMatch || "contains", userMatch: filterPresets.userMatch || "contains",
jobIdMatch: filterPresets.jobIdMatch || "eq", jobIdMatch: filterPresets.jobIdMatch || "eq",
nodeMatch: filterPresets.nodeMatch || "eq",
cluster: filterPresets.cluster || null, cluster: filterPresets.cluster || null,
partition: filterPresets.partition || null, partition: filterPresets.partition || null,
@ -106,7 +112,7 @@
let items = []; let items = [];
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } }); if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
if (filters.node) items.push({ node: { contains: filters.node } }); if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
if (filters.partition) items.push({ partition: { eq: filters.partition } }); if (filters.partition) items.push({ partition: { eq: filters.partition } });
if (filters.states.length != allJobStates.length) if (filters.states.length != allJobStates.length)
items.push({ state: filters.states }); items.push({ state: filters.states });
@ -178,6 +184,8 @@
let opts = []; let opts = [];
if (filters.cluster) opts.push(`cluster=${filters.cluster}`); if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
if (filters.node) opts.push(`node=${filters.node}`); if (filters.node) opts.push(`node=${filters.node}`);
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
opts.push(`nodeMatch=${filters.nodeMatch}`);
if (filters.partition) opts.push(`partition=${filters.partition}`); if (filters.partition) opts.push(`partition=${filters.partition}`);
if (filters.states.length != allJobStates.length) if (filters.states.length != allJobStates.length)
for (let state of filters.states) opts.push(`state=${state}`); for (let state of filters.states) opts.push(`state=${state}`);
@ -196,7 +204,7 @@
opts.push(`jobId=${singleJobId}`); opts.push(`jobId=${singleJobId}`);
} }
if (filters.jobIdMatch != "eq") if (filters.jobIdMatch != "eq")
opts.push(`jobIdMatch=${filters.jobIdMatch}`); opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case
for (let tag of filters.tags) opts.push(`tag=${tag}`); for (let tag of filters.tags) opts.push(`tag=${tag}`);
if (filters.duration.from && filters.duration.to) if (filters.duration.from && filters.duration.to)
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`); opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
@ -218,13 +226,13 @@
} else { } else {
for (let singleUser of filters.user) opts.push(`user=${singleUser}`); for (let singleUser of filters.user) opts.push(`user=${singleUser}`);
} }
if (filters.userMatch != "contains") if (filters.userMatch != "contains") // "contains" is default-case
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.project && filters.projectMatch != "contains") // "contains" is default-case
opts.push(`projectMatch=${filters.projectMatch}`);
if (filters.jobName) opts.push(`jobName=${filters.jobName}`); if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`); if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
if (filters.project && filters.projectMatch != "contains")
opts.push(`projectMatch=${filters.projectMatch}`);
if (filters.stats.length != 0) if (filters.stats.length != 0)
for (let stat of filters.stats) { for (let stat of filters.stats) {
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`); opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
@ -386,7 +394,7 @@
{#if filters.node != null} {#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> <Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node: {filters.node} Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node}
</Info> </Info>
{/if} {/if}
@ -449,6 +457,7 @@
bind:numHWThreads={filters.numHWThreads} bind:numHWThreads={filters.numHWThreads}
bind:numAccelerators={filters.numAccelerators} bind:numAccelerators={filters.numAccelerators}
bind:namedNode={filters.node} bind:namedNode={filters.node}
bind:nodeMatch={filters.nodeMatch}
bind:isNodesModified bind:isNodesModified
bind:isHwthreadsModified bind:isHwthreadsModified
bind:isAccsModified bind:isAccsModified

View File

@ -24,6 +24,7 @@
ModalBody, ModalBody,
ModalHeader, ModalHeader,
ModalFooter, ModalFooter,
Input
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
@ -40,11 +41,18 @@
export let isHwthreadsModified = false; export let isHwthreadsModified = false;
export let isAccsModified = false; export let isAccsModified = false;
export let namedNode = null; export let namedNode = null;
export let nodeMatch = "eq"
let pendingNumNodes = numNodes, let pendingNumNodes = numNodes,
pendingNumHWThreads = numHWThreads, pendingNumHWThreads = numHWThreads,
pendingNumAccelerators = numAccelerators, pendingNumAccelerators = numAccelerators,
pendingNamedNode = namedNode; pendingNamedNode = namedNode,
pendingNodeMatch = nodeMatch;
const nodeMatchLabels = {
eq: "Equal To",
contains: "Contains",
}
const findMaxNumAccels = (clusters) => const findMaxNumAccels = (clusters) =>
clusters.reduce( clusters.reduce(
@ -145,7 +153,17 @@
<ModalHeader>Select number of utilized Resources</ModalHeader> <ModalHeader>Select number of utilized Resources</ModalHeader>
<ModalBody> <ModalBody>
<h6>Named Node</h6> <h6>Named Node</h6>
<input type="text" class="form-control" bind:value={pendingNamedNode} /> <div class="d-flex">
<Input type="text" class="w-75" bind:value={pendingNamedNode} />
<div class="mx-1"></div>
<Input type="select" class="w-25" bind:value={pendingNodeMatch}>
{#each Object.entries(nodeMatchLabels) as [nodeMatchKey, nodeMatchLabel]}
<option value={nodeMatchKey}>
{nodeMatchLabel}
</option>
{/each}
</Input>
</div>
<h6 style="margin-top: 1rem;">Number of Nodes</h6> <h6 style="margin-top: 1rem;">Number of Nodes</h6>
<DoubleRangeSlider <DoubleRangeSlider
on:change={({ detail }) => { on:change={({ detail }) => {
@ -215,11 +233,13 @@
to: pendingNumAccelerators.to, to: pendingNumAccelerators.to,
}; };
namedNode = pendingNamedNode; namedNode = pendingNamedNode;
nodeMatch = pendingNodeMatch;
dispatch("set-filter", { dispatch("set-filter", {
numNodes, numNodes,
numHWThreads, numHWThreads,
numAccelerators, numAccelerators,
namedNode, namedNode,
nodeMatch
}); });
}} }}
> >
@ -233,6 +253,7 @@
pendingNumHWThreads = { from: null, to: null }; pendingNumHWThreads = { from: null, to: null };
pendingNumAccelerators = { from: null, to: null }; pendingNumAccelerators = { from: null, to: null };
pendingNamedNode = null; pendingNamedNode = null;
pendingNodeMatch = null;
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }; numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
numHWThreads = { numHWThreads = {
from: pendingNumHWThreads.from, from: pendingNumHWThreads.from,
@ -246,11 +267,13 @@
isHwthreadsModified = false; isHwthreadsModified = false;
isAccsModified = false; isAccsModified = false;
namedNode = pendingNamedNode; namedNode = pendingNamedNode;
nodeMatch = pendingNodeMatch;
dispatch("set-filter", { dispatch("set-filter", {
numNodes, numNodes,
numHWThreads, numHWThreads,
numAccelerators, numAccelerators,
namedNode, namedNode,
nodeMatch
}); });
}}>Reset</Button }}>Reset</Button
> >