Merge branch 'master' into 97_107_mark_and_show_shared

This commit is contained in:
Christoph Kluge
2023-06-01 15:24:26 +02:00
85 changed files with 4317 additions and 3839 deletions

View File

@@ -9,83 +9,144 @@
- update(filters?: [JobFilter])
-->
<script>
import { operationStore, query, mutation } from '@urql/svelte'
import { getContext } from 'svelte';
import { Row, Table, Card, Spinner } from 'sveltestrap'
import Pagination from './Pagination.svelte'
import JobListRow from './Row.svelte'
import { stickyHeader } from '../utils.js'
import {
queryStore,
gql,
getContextClient,
mutationStore,
} from "@urql/svelte";
import { getContext } from "svelte";
import { Row, Table, Card, Spinner } from "sveltestrap";
import Pagination from "./Pagination.svelte";
import JobListRow from "./Row.svelte";
import { stickyHeader } from "../utils.js";
const ccconfig = getContext('cc-config'),
clusters = getContext('clusters'),
initialized = getContext('initialized')
const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"),
initialized = getContext("initialized");
export let sorting = { field: "startTime", order: "DESC" }
export let matchedJobs = 0
export let metrics = ccconfig.plot_list_selectedMetrics
export let sorting = { field: "startTime", order: "DESC" };
export let matchedJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics;
let itemsPerPage = ccconfig.plot_list_jobsPerPage
let page = 1
let paging = { itemsPerPage, page }
let filter = []
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1;
let paging = { itemsPerPage, page };
let filter = [];
const jobs = operationStore(`
query($filter: [JobFilter!]!, $sorting: OrderByInput!, $paging: PageRequest! ){
jobs(filter: $filter, order: $sorting, page: $paging) {
items {
id, jobId, user, project, jobName, cluster, subCluster, startTime,
duration, numNodes, numHWThreads, numAcc, walltime, resources { hostname },
SMT, exclusive, partition, arrayJobId,
monitoringStatus, state,
tags { id, type, name }
userData { name }
metaData
const client = getContextClient();
const query = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
items {
id
jobId
user
project
jobName
cluster
subCluster
startTime
duration
numNodes
numHWThreads
numAcc
walltime
resources {
hostname
}
SMT
exclusive
partition
arrayJobId
monitoringStatus
state
tags {
id
type
name
}
userData {
name
}
metaData
}
count
}
count
}
}`, {
paging,
sorting,
filter,
}, {
pause: true
})
`;
const updateConfiguration = mutation({
query: `mutation($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}`
})
$: jobs = queryStore({
client: client,
query: query,
variables: { paging, sorting, filter }
});
$: $jobs.variables = { ...$jobs.variables, sorting, paging }
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0
$: matchedJobs = $jobs.data != null ? $jobs.data.jobs.count : 0;
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refresh() {
queryStore({
client: client,
query: query,
variables: { paging, sorting, filter },
requestPolicy: 'network-only'
});
}
// (Re-)query and optionally set new filters.
export function update(filters) {
if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor })
filters.push({ minRunningFor });
}
$jobs.variables.filter = filters
// console.log('filters:', ...filters.map(f => Object.entries(f)).flat(2))
filter = filters;
}
page = 1
$jobs.variables.paging = paging = { page, itemsPerPage };
$jobs.context.pause = false
$jobs.reexecute({ requestPolicy: 'network-only' })
page = 1;
paging = paging = { page, itemsPerPage };
}
query(jobs)
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value }
});
}
let tableWidth = null
let jobInfoColumnWidth = 250
$: plotWidth = Math.floor((tableWidth - jobInfoColumnWidth) / metrics.length - 10)
function updateConfiguration(value, page) {
updateConfigurationMutation({ name: 'plot_list_jobsPerPage', value: value })
.subscribe(res => {
if (res.fetching === false && !res.error) {
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
} else if (res.fetching === false && res.error) {
throw res.error
// console.log('Error on subscription: ' + res.error)
}
})
};
let headerPaddingTop = 0
stickyHeader('.cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)', (x) => (headerPaddingTop = x))
let tableWidth = null;
let jobInfoColumnWidth = 250;
$: plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / metrics.length - 10
);
let headerPaddingTop = 0;
stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x)
);
</script>
<Row>
@@ -93,20 +154,43 @@
<Table cellspacing="0px" cellpadding="0px">
<thead>
<tr>
<th class="position-sticky top-0" scope="col" style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px">
<th
class="position-sticky top-0"
scope="col"
style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px"
>
Job Info
</th>
{#each metrics as metric (metric)}
<th class="position-sticky top-0 text-center" scope="col" style="width: {plotWidth}px; padding-top: {headerPaddingTop}px">
<th
class="position-sticky top-0 text-center"
scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
>
{metric}
{#if $initialized}
({clusters
.map(cluster => cluster.metricConfig.find(m => m.name == metric))
.filter(m => m != null)
.map(m => (m.unit?.prefix?m.unit?.prefix:'') + (m.unit?.base?m.unit?.base:'')) // Build unitStr
.reduce((arr, unitStr) => arr.includes(unitStr) ? arr : [...arr, unitStr], []) // w/o this, output would be [unitStr, unitStr]
.join(', ')
})
.map((cluster) =>
cluster.metricConfig.find(
(m) => m.name == metric
)
)
.filter((m) => m != null)
.map(
(m) =>
(m.unit?.prefix
? m.unit?.prefix
: "") +
(m.unit?.base ? m.unit?.base : "")
) // Build unitStr
.reduce(
(arr, unitStr) =>
arr.includes(unitStr)
? arr
: [...arr, unitStr],
[]
) // w/o this, output would be [unitStr, unitStr]
.join(", ")})
{/if}
</th>
{/each}
@@ -115,28 +199,27 @@
<tbody>
{#if $jobs.error}
<tr>
<td colspan="{metrics.length + 1}">
<Card body color="danger" class="mb-3"><h2>{$jobs.error.message}</h2></Card>
<td colspan={metrics.length + 1}>
<Card body color="danger" class="mb-3"
><h2>{$jobs.error.message}</h2></Card
>
</td>
</tr>
{:else if $jobs.fetching || !$jobs.data}
<tr>
<td colspan="{metrics.length + 1}">
<td colspan={metrics.length + 1}>
<Spinner secondary />
</td>
</tr>
{:else if $jobs.data && $initialized}
{#each $jobs.data.jobs.items as job (job)}
<JobListRow
job={job}
metrics={metrics}
plotWidth={plotWidth} />
<JobListRow {job} {metrics} {plotWidth} />
{:else}
<tr>
<td colspan="{metrics.length + 1}">
No jobs found
</td>
</tr>
<tr>
<td colspan={metrics.length + 1}>
No jobs found
</td>
</tr>
{/each}
{/if}
</tbody>
@@ -145,24 +228,21 @@
</Row>
<Pagination
bind:page={page}
bind:page
{itemsPerPage}
itemText="Jobs"
totalItems={matchedJobs}
on:update={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) {
itemsPerPage = detail.itemsPerPage
updateConfiguration({
name: "plot_list_jobsPerPage",
value: itemsPerPage.toString()
}).then(res => {
if (res.error)
console.error(res.error);
})
updateConfiguration(
detail.itemsPerPage.toString(),
detail.page
)
} else {
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }
}
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }
}} />
}}
/>
<style>
.cc-table-wrapper {

View File

@@ -9,132 +9,180 @@
-->
<script>
import { operationStore, query } from '@urql/svelte'
import { getContext } from 'svelte'
import { Card, Spinner } from 'sveltestrap'
import MetricPlot from '../plots/MetricPlot.svelte'
import JobInfo from './JobInfo.svelte'
import { maxScope } from '../utils.js'
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext } from "svelte";
import { Card, Spinner } from "sveltestrap";
import MetricPlot from "../plots/MetricPlot.svelte";
import JobInfo from "./JobInfo.svelte";
import { maxScope } from "../utils.js";
export let job
export let metrics
export let plotWidth
export let plotHeight = 275
export let job;
export let metrics;
export let plotWidth;
export let plotHeight = 275;
let scopes = [job.numNodes == 1 ? 'core' : 'node']
let { id } = job;
let scopes = [job.numNodes == 1 ? "core" : "node"];
const cluster = getContext('clusters').find(c => c.name == job.cluster)
// Get all MetricConfs which include subCluster-specific settings for this job
const metricConfig = getContext('metrics')
const metricsQuery = operationStore(`query($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) {
name
scope
metric {
unit { prefix, base }, timestep
statisticsSeries { min, mean, max }
series {
hostname, id, data
statistics { min, avg, max }
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
const metricConfig = getContext("metrics"); // Get all MetricConfs which include subCluster-specific settings for this job
const client = getContextClient();
const query = gql`
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) {
name
scope
metric {
unit {
prefix
base
}
timestep
statisticsSeries {
min
mean
max
}
series {
hostname
id
data
statistics {
min
avg
max
}
}
}
}
}
}`, {
id: job.id,
metrics,
scopes
})
`;
const selectScope = (jobMetrics) => jobMetrics.reduce(
(a, b) => maxScope([a.scope, b.scope]) == a.scope
? (job.numNodes > 1 ? a : b)
: (job.numNodes > 1 ? b : a), jobMetrics[0])
$: metricsQuery = queryStore({
client: client,
query: query,
variables: { id, metrics, scopes }
});
const sortAndSelectScope = (jobMetrics) => metrics
.map(function(name) {
// Get MetricConf for this selected/requested metric
let thisConfig = metricConfig(cluster, name)
let thisSCIndex = thisConfig.subClusters.findIndex(sc => sc.name == job.subCluster)
// Check if Subcluster has MetricConf: If not found (index == -1), no further remove flag check required
if (thisSCIndex >= 0) {
// SubCluster Config present: Check if remove flag is set
if (thisConfig.subClusters[thisSCIndex].remove == true) {
// Return null data and informational flag
return {removed: true, data: null}
} else {
// load and return metric, if data available
let thisMetric = jobMetrics.filter(jobMetric => jobMetric.name == name) // Returns Array
if (thisMetric.length > 0) {
return {removed: false, data: thisMetric}
function refresh() {
queryStore({
client: client,
query: query,
variables: { id, metrics, scopes }
});
}
const selectScope = (jobMetrics) =>
jobMetrics.reduce(
(a, b) =>
maxScope([a.scope, b.scope]) == a.scope
? job.numNodes > 1
? a
: b
: job.numNodes > 1
? b
: a,
jobMetrics[0]
);
const sortAndSelectScope = (jobMetrics) =>
metrics
.map(function (name) {
// Get MetricConf for this selected/requested metric
let thisConfig = metricConfig(cluster, name);
let thisSCIndex = -1
if (thisConfig) {
thisSCIndex = thisConfig.subClusters.findIndex(
(sc) => sc.name == job.subCluster
);
};
// Check if Subcluster has MetricConf: If not found (index == -1), no further remove flag check required
if (thisSCIndex >= 0) {
// SubCluster Config present: Check if remove flag is set
if (thisConfig.subClusters[thisSCIndex].remove == true) {
// Return null data and informational flag
return { removed: true, data: null };
} else {
return {removed: false, data: null}
// load and return metric, if data available
let thisMetric = jobMetrics.filter(
(jobMetric) => jobMetric.name == name
); // Returns Array
if (thisMetric.length > 0) {
return { removed: false, data: thisMetric };
} else {
return { removed: false, data: null };
}
}
} else {
// No specific subCluster config: 'remove' flag not set, deemed false -> load and return metric, if data available
let thisMetric = jobMetrics.filter(
(jobMetric) => jobMetric.name == name
); // Returns Array
if (thisMetric.length > 0) {
return { removed: false, data: thisMetric };
} else {
return { removed: false, data: null };
}
}
} else {
// No specific subCluster config: 'remove' flag not set, deemed false -> load and return metric, if data available
let thisMetric = jobMetrics.filter(jobMetric => jobMetric.name == name) // Returns Array
if (thisMetric.length > 0) {
return {removed: false, data: thisMetric}
})
.map(function (jobMetrics) {
if (jobMetrics.data != null && jobMetrics.data.length > 0) {
return {
removed: jobMetrics.removed,
data: selectScope(jobMetrics.data),
};
} else {
return {removed: false, data: null}
return jobMetrics;
}
}
})
.map(function(jobMetrics) {
if (jobMetrics.data != null && jobMetrics.data.length > 0) {
return {removed: jobMetrics.removed, data: selectScope(jobMetrics.data)}
} else {
return jobMetrics
}
})
});
$: metricsQuery.variables = { id: job.id, metrics, scopes }
if (job.monitoringStatus)
query(metricsQuery)
if (job.monitoringStatus) refresh();
</script>
<tr>
<td>
<JobInfo job={job}/>
<JobInfo {job} />
</td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
<td colspan="{metrics.length}">
<td colspan={metrics.length}>
<Card body color="warning">Not monitored or archiving failed</Card>
</td>
{:else if $metricsQuery.fetching}
<td colspan="{metrics.length}" style="text-align: center;">
<td colspan={metrics.length} style="text-align: center;">
<Spinner secondary />
</td>
{:else if $metricsQuery.error}
<td colspan="{metrics.length}">
<td colspan={metrics.length}>
<Card body color="danger" class="mb-3">
{$metricsQuery.error.message.length > 500
? $metricsQuery.error.message.substring(0, 499)+'...'
? $metricsQuery.error.message.substring(0, 499) + "..."
: $metricsQuery.error.message}
</Card>
</td>
{:else}
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
<td>
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
{#if metric.removed == false && metric.data != null}
<MetricPlot
width={plotWidth}
height={plotHeight}
timestep={metric.data.metric.timestep}
scope={metric.data.scope}
series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.data.name}
cluster={cluster}
subCluster={job.subCluster}
isShared={(job.exclusive != 1)}/>
{:else if metric.removed == true && metric.data == null}
<Card body color="info">Metric disabled for subcluster '{ job.subCluster }'</Card>
{:else}
<Card body color="warning">Missing Data</Card>
{/if}
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
{#if metric.removed == false && metric.data != null}
<MetricPlot
width={plotWidth}
height={plotHeight}
timestep={metric.data.metric.timestep}
scope={metric.data.scope}
series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.data.name}
{cluster}
subCluster={job.subCluster}
isShared={(job.exclusive != 1)}
/>
{:else if metric.removed == true && metric.data == null}
<Card body color="info"
>Metric disabled for subcluster '{job.subCluster}'</Card
>
{:else}
<Card body color="warning">Missing Data</Card>
{/if}
</td>
{/each}
{/if}