Start to fix errors with urql 4

This commit is contained in:
Jan Eitzinger 2023-05-05 10:07:12 +02:00
parent bb20ed655a
commit 52738c7f8e
5 changed files with 506 additions and 395 deletions

View File

@ -5,8 +5,8 @@ import resolve from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser'; import terser from '@rollup/plugin-terser';
import css from 'rollup-plugin-css-only'; import css from 'rollup-plugin-css-only';
// const production = !process.env.ROLLUP_WATCH; const production = !process.env.ROLLUP_WATCH;
const production = false // const production = false
const plugins = [ const plugins = [
svelte({ svelte({

View File

@ -4,12 +4,20 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { init } from "./utils.js"; import { init } from "./utils.js";
import { Row, Col, Button, Icon, Table, import {
Card, Spinner, InputGroup, Input, } from "sveltestrap"; Row,
Col,
Button,
Icon,
Table,
Card,
Spinner,
InputGroup,
Input,
} from "sveltestrap";
import Filters from "./filters/Filters.svelte"; import Filters from "./filters/Filters.svelte";
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/svelte";
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte"; import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
import { UniqueInputFieldNamesRule } from "graphql";
const {} = init(); const {} = init();
@ -21,9 +29,9 @@
"Invalid list type provided!" "Invalid list type provided!"
); );
let filter = [] let filter = [];
$: stats = queryStore({ const stats = queryStore({
client: getContextClient(), client: getContextClient(),
query: gql` query: gql`
query($filter: [JobFilter!]!) { query($filter: [JobFilter!]!) {
@ -36,7 +44,7 @@
} }
}`, }`,
variables: { filter }, variables: { filter },
pause: true pause: true,
}); });
let filters; let filters;
@ -92,7 +100,7 @@
startTimeQuickSelect={true} startTimeQuickSelect={true}
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up" menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
on:update={({ detail }) => { on:update={({ detail }) => {
$stats.variables = { filter: detail.filters } filter = detail.filters;
stats.resume(); stats.resume();
}} }}
/> />
@ -102,7 +110,10 @@
<thead> <thead>
<tr> <tr>
<th scope="col"> <th scope="col">
{({ USER: "Username", PROJECT: "Project Name" })[type]} <!-- {({ -->
<!-- USER: "Username", -->
<!-- PROJECT: "Project Name", -->
<!-- })[type]} -->
<Button <Button
color={sorting.field == "id" ? "primary" : "light"} color={sorting.field == "id" ? "primary" : "light"}
size="sm" size="sm"

View File

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

View File

@ -9,136 +9,173 @@
--> -->
<script> <script>
import { queryStore, gql, getContextClient } from '@urql/svelte' import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext } from 'svelte' import { getContext } from "svelte";
import { Card, Spinner } from 'sveltestrap' import { Card, Spinner } from "sveltestrap";
import MetricPlot from '../plots/MetricPlot.svelte' import MetricPlot from "../plots/MetricPlot.svelte";
import JobInfo from './JobInfo.svelte' import JobInfo from "./JobInfo.svelte";
import { maxScope } from '../utils.js' import { maxScope } from "../utils.js";
export let job export let job;
export let metrics export let metrics;
export let plotWidth export let plotWidth;
export let plotHeight = 275 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) const cluster = getContext("clusters").find((c) => c.name == job.cluster);
// Get all MetricConfs which include subCluster-specific settings for this job // Get all MetricConfs which include subCluster-specific settings for this job
const metricConfig = getContext('metrics') const metricConfig = getContext("metrics");
const metricsQuery = queryStore({ const metricsQuery = queryStore({
client: getContextClient(), client: getContextClient(),
query: gql` query: gql`
query($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) { query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) { jobMetrics(id: $id, metrics: $metrics, scopes: $scopes) {
name name
scope scope
metric { metric {
unit { prefix, base }, timestep unit {
statisticsSeries { min, mean, max } prefix
series { base
hostname, id, data }
statistics { min, avg, max } timestep
} statisticsSeries {
} min
} mean
}`, max
pause: true, }
variables: { series {
id: job.id, hostname
metrics, id
scopes} data
}) statistics {
min
const selectScope = (jobMetrics) => jobMetrics.reduce( avg
(a, b) => maxScope([a.scope, b.scope]) == a.scope max
? (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 = 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}
} 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 pause: true,
if (thisMetric.length > 0) { variables: {
return {removed: false, data: thisMetric} 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 = 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 };
} else {
return { removed: false, data: null };
}
}
} else { } else {
return {removed: false, data: null} // 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 };
}
} }
} })
}) .map(function (jobMetrics) {
.map(function(jobMetrics) { if (jobMetrics.data != null && jobMetrics.data.length > 0) {
if (jobMetrics.data != null && jobMetrics.data.length > 0) { return {
return {removed: jobMetrics.removed, data: selectScope(jobMetrics.data)} removed: jobMetrics.removed,
} else { data: selectScope(jobMetrics.data),
return jobMetrics };
} } else {
}) return jobMetrics;
}
});
$: metricsQuery.variables = { id: job.id, metrics, scopes } // $: metricsQuery.variables = { id: job.id, metrics, scopes };
if (job.monitoringStatus) if (job.monitoringStatus) metricsQuery.resume();
$metricsQuery.resume()
</script> </script>
<tr> <tr>
<td> <td>
<JobInfo job={job}/> <JobInfo {job} />
</td> </td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2} {#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> <Card body color="warning">Not monitored or archiving failed</Card>
</td> </td>
{:else if $metricsQuery.fetching} {:else if $metricsQuery.fetching}
<td colspan="{metrics.length}" style="text-align: center;"> <td colspan={metrics.length} style="text-align: center;">
<Spinner secondary /> <Spinner secondary />
</td> </td>
{:else if $metricsQuery.error} {:else if $metricsQuery.error}
<td colspan="{metrics.length}"> <td colspan={metrics.length}>
<Card body color="danger" class="mb-3"> <Card body color="danger" class="mb-3">
{$metricsQuery.error.message.length > 500 {$metricsQuery.error.message.length > 500
? $metricsQuery.error.message.substring(0, 499)+'...' ? $metricsQuery.error.message.substring(0, 499) + "..."
: $metricsQuery.error.message} : $metricsQuery.error.message}
</Card> </Card>
</td> </td>
{:else} {:else}
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)} {#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
<td> <td>
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case--> <!-- 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} {#if metric.removed == false && metric.data != null}
<MetricPlot <MetricPlot
width={plotWidth} width={plotWidth}
height={plotHeight} height={plotHeight}
timestep={metric.data.metric.timestep} timestep={metric.data.metric.timestep}
scope={metric.data.scope} scope={metric.data.scope}
series={metric.data.metric.series} series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries} statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.data.name} metric={metric.data.name}
cluster={cluster} {cluster}
subCluster={job.subCluster} /> subCluster={job.subCluster}
{:else if metric.removed == true && metric.data == null} />
<Card body color="info">Metric disabled for subcluster '{ job.subCluster }'</Card> {:else if metric.removed == true && metric.data == null}
{:else} <Card body color="info"
<Card body color="warning">Missing Data</Card> >Metric disabled for subcluster '{job.subCluster}'</Card
{/if} >
{:else}
<Card body color="warning">Missing Data</Card>
{/if}
</td> </td>
{/each} {/each}
{/if} {/if}

View File

@ -1,8 +1,8 @@
import { expiringCacheExchange } from "./cache-exchange.js"; import { expiringCacheExchange } from "./cache-exchange.js";
import { import {
CreateClient, Client,
setContextClient, setContextClient,
fetchExchange, fetchExchange,
} from "@urql/svelte"; } from "@urql/svelte";
import { setContext, getContext, hasContext, onDestroy, tick } from "svelte"; import { setContext, getContext, hasContext, onDestroy, tick } from "svelte";
import { readable } from "svelte/store"; import { readable } from "svelte/store";
@ -18,28 +18,28 @@ import { readable } from "svelte/store";
* - Adds 'metrics' to the context, a function that takes a cluster and metric name and returns the MetricConfig (or undefined) * - Adds 'metrics' to the context, a function that takes a cluster and metric name and returns the MetricConfig (or undefined)
*/ */
export function init(extraInitQuery = "") { export function init(extraInitQuery = "") {
const jwt = hasContext("jwt") const jwt = hasContext("jwt")
? getContext("jwt") ? getContext("jwt")
: getContext("cc-config")["jwt"]; : getContext("cc-config")["jwt"];
const client = CreateClient({ const client = new Client({
url: `${window.location.origin}/query`, url: `${window.location.origin}/query`,
fetchOptions: fetchOptions:
jwt != null ? { headers: { Authorization: `Bearer ${jwt}` } } : {}, jwt != null ? { headers: { Authorization: `Bearer ${jwt}` } } : {},
exchanges: [ exchanges: [
expiringCacheExchange({ expiringCacheExchange({
ttl: 5 * 60 * 1000, ttl: 5 * 60 * 1000,
maxSize: 150, maxSize: 150,
}), }),
fetchExchange, fetchExchange,
], ],
}); });
setContextClient(client); setContextClient(client);
const query = client const query = client
.query( .query(
`query { `query {
clusters { clusters {
name, name,
metricConfig { metricConfig {
@ -68,246 +68,246 @@ export function init(extraInitQuery = "") {
tags { id, name, type } tags { id, name, type }
${extraInitQuery} ${extraInitQuery}
}` }`
) )
.toPromise(); .toPromise();
let state = { fetching: true, error: null, data: null }; let state = { fetching: true, error: null, data: null };
let subscribers = []; let subscribers = [];
const subscribe = (callback) => { const subscribe = (callback) => {
callback(state); callback(state);
subscribers.push(callback); subscribers.push(callback);
return () => { return () => {
subscribers = subscribers.filter((cb) => cb != callback); subscribers = subscribers.filter((cb) => cb != callback);
};
}; };
};
const tags = [], const tags = [],
clusters = []; clusters = [];
setContext("tags", tags); setContext("tags", tags);
setContext("clusters", clusters); setContext("clusters", clusters);
setContext("metrics", (cluster, metric) => { setContext("metrics", (cluster, metric) => {
if (typeof cluster !== "object") if (typeof cluster !== "object")
cluster = clusters.find((c) => c.name == cluster); cluster = clusters.find((c) => c.name == cluster);
return cluster.metricConfig.find((m) => m.name == metric); return cluster.metricConfig.find((m) => m.name == metric);
}); });
setContext("on-init", (callback) => setContext("on-init", (callback) =>
state.fetching ? subscribers.push(callback) : callback(state) state.fetching ? subscribers.push(callback) : callback(state)
); );
setContext( setContext(
"initialized", "initialized",
readable(false, (set) => subscribers.push(() => set(true))) readable(false, (set) => subscribers.push(() => set(true)))
); );
query.then(({ error, data }) => { query.then(({ error, data }) => {
state.fetching = false; state.fetching = false;
if (error != null) { if (error != null) {
console.error(error); console.error(error);
state.error = error; state.error = error;
tick().then(() => subscribers.forEach((cb) => cb(state))); tick().then(() => subscribers.forEach((cb) => cb(state)));
return; return;
} }
for (let tag of data.tags) tags.push(tag); for (let tag of data.tags) tags.push(tag);
for (let cluster of data.clusters) clusters.push(cluster); for (let cluster of data.clusters) clusters.push(cluster);
state.data = data; state.data = data;
tick().then(() => subscribers.forEach((cb) => cb(state))); tick().then(() => subscribers.forEach((cb) => cb(state)));
}); });
return { return {
query: { subscribe }, query: { subscribe },
tags, tags,
clusters, clusters,
}; };
} }
export function formatNumber(x) { export function formatNumber(x) {
let suffix = ""; let suffix = "";
if (x >= 1000000000) { if (x >= 1000000000) {
x /= 1000000; x /= 1000000;
suffix = "G"; suffix = "G";
} else if (x >= 1000000) { } else if (x >= 1000000) {
x /= 1000000; x /= 1000000;
suffix = "M"; suffix = "M";
} else if (x >= 1000) { } else if (x >= 1000) {
x /= 1000; x /= 1000;
suffix = "k"; suffix = "k";
} }
return `${Math.round(x * 100) / 100} ${suffix}`; return `${Math.round(x * 100) / 100} ${suffix}`;
} }
// Use https://developer.mozilla.org/en-US/docs/Web/API/structuredClone instead? // Use https://developer.mozilla.org/en-US/docs/Web/API/structuredClone instead?
export function deepCopy(x) { export function deepCopy(x) {
return JSON.parse(JSON.stringify(x)); return JSON.parse(JSON.stringify(x));
} }
function fuzzyMatch(term, string) { function fuzzyMatch(term, string) {
return string.toLowerCase().includes(term); return string.toLowerCase().includes(term);
} }
export function fuzzySearchTags(term, tags) { export function fuzzySearchTags(term, tags) {
if (!tags) return []; if (!tags) return [];
let results = []; let results = [];
let termparts = term let termparts = term
.split(":") .split(":")
.map((s) => s.trim()) .map((s) => s.trim())
.filter((s) => s.length > 0); .filter((s) => s.length > 0);
if (termparts.length == 0) { if (termparts.length == 0) {
results = tags.slice(); results = tags.slice();
} else if (termparts.length == 1) { } else if (termparts.length == 1) {
for (let tag of tags) for (let tag of tags)
if ( if (
fuzzyMatch(termparts[0], tag.type) || fuzzyMatch(termparts[0], tag.type) ||
fuzzyMatch(termparts[0], tag.name) fuzzyMatch(termparts[0], tag.name)
) )
results.push(tag); results.push(tag);
} else if (termparts.length == 2) { } else if (termparts.length == 2) {
for (let tag of tags) for (let tag of tags)
if ( if (
fuzzyMatch(termparts[0], tag.type) && fuzzyMatch(termparts[0], tag.type) &&
fuzzyMatch(termparts[1], tag.name) fuzzyMatch(termparts[1], tag.name)
) )
results.push(tag); results.push(tag);
} }
return results.sort((a, b) => { return results.sort((a, b) => {
if (a.type < b.type) return -1; if (a.type < b.type) return -1;
if (a.type > b.type) return 1; if (a.type > b.type) return 1;
if (a.name < b.name) return -1; if (a.name < b.name) return -1;
if (a.name > b.name) return 1; if (a.name > b.name) return 1;
return 0; return 0;
}); });
} }
export function groupByScope(jobMetrics) { export function groupByScope(jobMetrics) {
let metrics = new Map(); let metrics = new Map();
for (let metric of jobMetrics) { for (let metric of jobMetrics) {
if (metrics.has(metric.name)) metrics.get(metric.name).push(metric); if (metrics.has(metric.name)) metrics.get(metric.name).push(metric);
else metrics.set(metric.name, [metric]); else metrics.set(metric.name, [metric]);
} }
return [...metrics.values()].sort((a, b) => return [...metrics.values()].sort((a, b) =>
a[0].name.localeCompare(b[0].name) a[0].name.localeCompare(b[0].name)
); );
} }
const scopeGranularity = { const scopeGranularity = {
node: 10, node: 10,
socket: 5, socket: 5,
accelerator: 5, accelerator: 5,
core: 2, core: 2,
hwthread: 1, hwthread: 1,
}; };
export function maxScope(scopes) { export function maxScope(scopes) {
console.assert( console.assert(
scopes.length > 0 && scopes.every((x) => scopeGranularity[x] != null) scopes.length > 0 && scopes.every((x) => scopeGranularity[x] != null)
); );
let sm = scopes[0], let sm = scopes[0],
gran = scopeGranularity[scopes[0]]; gran = scopeGranularity[scopes[0]];
for (let scope of scopes) { for (let scope of scopes) {
let otherGran = scopeGranularity[scope]; let otherGran = scopeGranularity[scope];
if (otherGran > gran) { if (otherGran > gran) {
sm = scope; sm = scope;
gran = otherGran; gran = otherGran;
}
} }
} return sm;
return sm;
} }
export function minScope(scopes) { export function minScope(scopes) {
console.assert( console.assert(
scopes.length > 0 && scopes.every((x) => scopeGranularity[x] != null) scopes.length > 0 && scopes.every((x) => scopeGranularity[x] != null)
); );
let sm = scopes[0], let sm = scopes[0],
gran = scopeGranularity[scopes[0]]; gran = scopeGranularity[scopes[0]];
for (let scope of scopes) { for (let scope of scopes) {
let otherGran = scopeGranularity[scope]; let otherGran = scopeGranularity[scope];
if (otherGran < gran) { if (otherGran < gran) {
sm = scope; sm = scope;
gran = otherGran; gran = otherGran;
}
} }
} return sm;
return sm;
} }
export async function fetchMetrics(job, metrics, scopes) { export async function fetchMetrics(job, metrics, scopes) {
if (job.monitoringStatus == 0) return null; if (job.monitoringStatus == 0) return null;
let query = []; let query = [];
if (metrics != null) { if (metrics != null) {
for (let metric of metrics) { for (let metric of metrics) {
query.push(`metric=${metric}`); query.push(`metric=${metric}`);
}
} }
} if (scopes != null) {
if (scopes != null) { for (let scope of scopes) {
for (let scope of scopes) { query.push(`scope=${scope}`);
query.push(`scope=${scope}`); }
}
}
try {
let res = await fetch(
`/api/jobs/metrics/${job.id}${query.length > 0 ? "?" : ""}${query.join(
"&"
)}`
);
if (res.status != 200) {
return { error: { status: res.status, message: await res.text() } };
} }
return await res.json(); try {
} catch (e) { let res = await fetch(
return { error: e }; `/api/jobs/metrics/${job.id}${query.length > 0 ? "?" : ""}${query.join(
} "&"
)}`
);
if (res.status != 200) {
return { error: { status: res.status, message: await res.text() } };
}
return await res.json();
} catch (e) {
return { error: e };
}
} }
export function fetchMetricsStore() { export function fetchMetricsStore() {
let set = null; let set = null;
let prev = { fetching: true, error: null, data: null }; let prev = { fetching: true, error: null, data: null };
return [ return [
readable(prev, (_set) => { readable(prev, (_set) => {
set = _set; set = _set;
}), }),
(job, metrics, scopes) => (job, metrics, scopes) =>
fetchMetrics(job, metrics, scopes).then((res) => { fetchMetrics(job, metrics, scopes).then((res) => {
let next = { fetching: false, error: res.error, data: res.data }; let next = { fetching: false, error: res.error, data: res.data };
if (prev.data && next.data) if (prev.data && next.data)
next.data.jobMetrics.push(...prev.data.jobMetrics); next.data.jobMetrics.push(...prev.data.jobMetrics);
prev = next; prev = next;
set(next); set(next);
}), }),
]; ];
} }
export function stickyHeader(datatableHeaderSelector, updatePading) { export function stickyHeader(datatableHeaderSelector, updatePading) {
const header = document.querySelector("header > nav.navbar"); const header = document.querySelector("header > nav.navbar");
if (!header) return; if (!header) return;
let ticking = false, let ticking = false,
datatableHeader = null; datatableHeader = null;
const onscroll = (event) => { const onscroll = (event) => {
if (ticking) return; if (ticking) return;
ticking = true; ticking = true;
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
ticking = false; ticking = false;
if (!datatableHeader) if (!datatableHeader)
datatableHeader = document.querySelector(datatableHeaderSelector); datatableHeader = document.querySelector(datatableHeaderSelector);
const top = datatableHeader.getBoundingClientRect().top; const top = datatableHeader.getBoundingClientRect().top;
updatePading( updatePading(
top < header.clientHeight ? header.clientHeight - top + 10 : 10 top < header.clientHeight ? header.clientHeight - top + 10 : 10
); );
}); });
}; };
document.addEventListener("scroll", onscroll); document.addEventListener("scroll", onscroll);
onDestroy(() => document.removeEventListener("scroll", onscroll)); onDestroy(() => document.removeEventListener("scroll", onscroll));
} }