Migrate jobList and jobListRow

This commit is contained in:
Christoph Kluge 2025-06-27 15:52:54 +02:00
parent c8fe81cd80
commit c4c422da57
11 changed files with 208 additions and 185 deletions

View File

@ -562,6 +562,7 @@
</Row> </Row>
<Row> <Row>
<Col> <Col>
<!-- Note: Ignore '#snippet' Error in IDE -->
{#snippet histoGridContent(item)} {#snippet histoGridContent(item)}
<Histogram <Histogram
data={convert2uplot(item.bins)} data={convert2uplot(item.bins)}

View File

@ -315,7 +315,7 @@
</Col> </Col>
</Row> </Row>
{:else if $initq?.data && $jobMetrics?.data?.scopedJobStats} {:else if $initq?.data && $jobMetrics?.data?.scopedJobStats}
<!-- Note: Ignore 'Expected If ...' Error in IDE --> <!-- Note: Ignore '#snippet' Error in IDE -->
{#snippet gridContent(item)} {#snippet gridContent(item)}
{#if item.data} {#if item.data}
<Metric <Metric

View File

@ -178,18 +178,18 @@
{#if !showCompare} {#if !showCompare}
<JobList <JobList
bind:this={jobList} bind:this={jobList}
bind:metrics
bind:sorting
bind:matchedListJobs bind:matchedListJobs
bind:showFootprint
bind:selectedJobs bind:selectedJobs
{metrics}
{sorting}
{showFootprint}
{filterBuffer} {filterBuffer}
/> />
{:else} {:else}
<JobCompare <JobCompare
bind:this={jobCompare} bind:this={jobCompare}
bind:metrics
bind:matchedCompareJobs bind:matchedCompareJobs
{metrics}
{filterBuffer} {filterBuffer}
/> />
{/if} {/if}
@ -201,7 +201,8 @@
presetSorting={sorting} presetSorting={sorting}
applySorting={(newSort) => applySorting={(newSort) =>
sorting = {...newSort} sorting = {...newSort}
}/> }
/>
<MetricSelection <MetricSelection
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}

View File

@ -204,6 +204,7 @@
{:else if $nodeMetricsData.fetching || $initq.fetching} {:else if $nodeMetricsData.fetching || $initq.fetching}
<Spinner /> <Spinner />
{:else} {:else}
<!-- Note: Ignore '#snippet' Error in IDE -->
{#snippet gridContent(item)} {#snippet gridContent(item)}
<h4 style="text-align: center; padding-top:15px;"> <h4 style="text-align: center; padding-top:15px;">
{item.name} {item.name}

View File

@ -676,6 +676,7 @@
<!-- Selectable Stats as Histograms : Average Values of Running Jobs --> <!-- Selectable Stats as Histograms : Average Values of Running Jobs -->
{#if selectedHistograms} {#if selectedHistograms}
<!-- Note: Ignore '#snippet' Error in IDE -->
{#snippet gridContent(item)} {#snippet gridContent(item)}
<Histogram <Histogram
data={convert2uplot(item.data)} data={convert2uplot(item.data)}

View File

@ -335,6 +335,7 @@
</Row> </Row>
{:else} {:else}
<hr class="my-2"/> <hr class="my-2"/>
<!-- Note: Ignore '#snippet' Error in IDE -->
{#snippet gridContent(item)} {#snippet gridContent(item)}
<Histogram <Histogram
data={convert2uplot(item.data)} data={convert2uplot(item.data)}
@ -369,9 +370,9 @@
<JobList <JobList
bind:this={jobList} bind:this={jobList}
bind:matchedListJobs bind:matchedListJobs
bind:metrics {metrics}
bind:sorting {sorting}
bind:showFootprint {showFootprint}
/> />
</Col> </Col>
</Row> </Row>
@ -390,10 +391,10 @@
presetMetrics={metrics} presetMetrics={metrics}
cluster={selectedCluster} cluster={selectedCluster}
configName="plot_list_selectedMetrics" configName="plot_list_selectedMetrics"
footprintSelect
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
metrics = [...newMetrics] metrics = [...newMetrics]
} }
footprintSelect
/> />
<HistogramSelection <HistogramSelection

View File

@ -26,7 +26,7 @@
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
matchedCompareJobs = $bindable(0), matchedCompareJobs = $bindable(0),
metrics = $bindable(ccconfig?.plot_list_selectedMetrics), metrics = ccconfig?.plot_list_selectedMetrics,
filterBuffer = [], filterBuffer = [],
} = $props(); } = $props();

View File

@ -26,35 +26,22 @@
import Pagination from "./joblist/Pagination.svelte"; import Pagination from "./joblist/Pagination.svelte";
import JobListRow from "./joblist/JobListRow.svelte"; import JobListRow from "./joblist/JobListRow.svelte";
const ccconfig = getContext("cc-config"), /* Svelte 5 Props */
initialized = getContext("initialized"), let {
globalMetrics = getContext("globalMetrics"); matchedListJobs = $bindable(0),
selectedJobs = $bindable([]),
const equalsCheck = (a, b) => { metrics = getContext("cc-config").plot_list_selectedMetrics,
return JSON.stringify(a) === JSON.stringify(b); sorting = { field: "startTime", type: "col", order: "DESC" },
} showFootprint = false,
filterBuffer = [],
export let sorting = { field: "startTime", type: "col", order: "DESC" }; } = $props();
export let matchedListJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics;
export let showFootprint;
export let filterBuffer = [];
export let selectedJobs = [];
let usePaging = ccconfig.job_list_usePaging
let itemsPerPage = usePaging ? ccconfig.plot_list_jobsPerPage : 10;
let page = 1;
let paging = { itemsPerPage, page };
let filter = [...filterBuffer];
let lastFilter = [];
let lastSorting = null;
let triggerMetricRefresh = false;
function getUnit(m) {
const rawUnit = globalMetrics.find((gm) => gm.name === m)?.unit
return (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
}
/* Const Init */
const ccconfig = getContext("cc-config");
const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
const usePaging = ccconfig?.job_list_usePaging || false;
const jobInfoColumnWidth = 250;
const client = getContextClient(); const client = getContextClient();
const query = gql` const query = gql`
query ( query (
@ -107,14 +94,72 @@
} }
`; `;
$: jobsStore = queryStore({ /* Var Init */
let lastFilter = [];
let lastSorting = null;
/* State Init */
let headerPaddingTop = $state(0);
let jobs = $state([]);
let filter = $state([...filterBuffer]);
let page = $state(1);
let itemsPerPage = $state(usePaging ? (ccconfig?.plot_list_jobsPerPag || 10) : 10);
let triggerMetricRefresh = $state(false);
let tableWidth = $state(0);
/* Derived */
let paging = $derived({ itemsPerPage, page });
const plotWidth = $derived.by(() => {
return Math.floor(
(tableWidth - jobInfoColumnWidth) / (metrics.length + (showFootprint ? 1 : 0)) - 10,
);
});
let jobsStore = $derived(queryStore({
client: client, client: client,
query: query, query: query,
variables: { paging, sorting, filter }, variables: { paging, sorting, filter },
})
);
/* Effects */
$effect(() => {
if ($jobsStore?.data) {
matchedListJobs = $jobsStore.data.jobs.count;
} else {
matchedListJobs = -1
}
}); });
let jobs = []; $effect(() => {
$: if ($initialized && $jobsStore.data) { if (!usePaging) {
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $jobsStore?.data?.jobs?.hasNextPage) {
page += 1
};
});
};
});
$effect(() => {
// Triggers (Except Paging)
sorting
filter
// Continous Scroll: Reset jobs and paging if parameters change: Existing entries will not match new selections
if (!usePaging) {
jobs = [];
page = 1;
}
});
$effect(() => {
if ($initialized && $jobsStore?.data) {
if (usePaging) { if (usePaging) {
jobs = [...$jobsStore.data.jobs.items] jobs = [...$jobsStore.data.jobs.items]
} else { // Prevents jump to table head in continiuous mode, only if no change in sort or filter } else { // Prevents jump to table head in continiuous mode, only if no change in sort or filter
@ -136,21 +181,15 @@
jobs = [...$jobsStore.data.jobs.items] jobs = [...$jobsStore.data.jobs.items]
} }
} }
} };
});
$: if (!usePaging && (sorting || filter)) {
// Continous Scroll: Reset list and paging if parameters change: Existing entries will not match new selections
jobs = [];
paging = { itemsPerPage: 10, page: 1 };
}
$: matchedListJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
/* Functions */
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity) // Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refreshJobs() { export function refreshJobs() {
if (!usePaging) { if (!usePaging) {
jobs = []; // Empty Joblist before refresh, prevents infinite buildup jobs = []; // Empty Joblist before refresh, prevents infinite buildup
paging = { itemsPerPage: 10, page: 1 }; page = 1;
} }
jobsStore = queryStore({ jobsStore = queryStore({
client: client, client: client,
@ -178,8 +217,26 @@
filter = filters; filter = filters;
} }
page = 1; page = 1;
paging = paging = { page, itemsPerPage }; };
function updateConfiguration(value, newPage) {
updateConfigurationMutation({
name: "plot_list_jobsPerPage",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
jobs = [] // Empty List
paging = { itemsPerPage: value, page: newPage }; // Trigger reload of jobList
} else if (res.fetching === false && res.error) {
throw res.error;
} }
});
};
function getUnit(m) {
const rawUnit = globalMetrics.find((gm) => gm.name === m)?.unit
return (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
};
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@ -193,52 +250,11 @@
}); });
}; };
function updateConfiguration(value, page) { const equalsCheck = (a, b) => {
updateConfigurationMutation({ return JSON.stringify(a) === JSON.stringify(b);
name: "plot_list_jobsPerPage",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
jobs = [] // Empty List
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
} else if (res.fetching === false && res.error) {
throw res.error;
}
});
} }
if (!usePaging) { /* Init Header */
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $jobsStore.data != null && $jobsStore.data.jobs.hasNextPage) {
let pendingPaging = { ...paging }
pendingPaging.page += 1
paging = pendingPaging
};
});
};
let plotWidth = null;
let tableWidth = null;
let jobInfoColumnWidth = 250;
$: if (showFootprint) {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10,
);
} else {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / metrics.length - 10,
);
}
let headerPaddingTop = 0;
stickyHeader( stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)", ".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x), (x) => (headerPaddingTop = x),
@ -292,8 +308,8 @@
{:else} {:else}
{#each jobs as job (job.id)} {#each jobs as job (job.id)}
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)} <JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]} selectJob={(detail) => selectedJobs = [...selectedJobs, detail]}
on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)} unselectJob={(detail) => selectedJobs = selectedJobs.filter(item => item !== detail)}
/> />
{:else} {:else}
<tr> <tr>

View File

@ -20,7 +20,7 @@
username = null, username = null,
authlevel= null, authlevel= null,
roles = null, roles = null,
isSelected = null, isSelected = $bindable(),
showSelect = false, showSelect = false,
} = $props(); } = $props();
@ -89,10 +89,8 @@
}}> }}>
{#if isSelected} {#if isSelected}
<Icon name="check-square"/> <Icon name="check-square"/>
{:else if isSelected == false} {:else }
<Icon name="square"/> <Icon name="plus-square-dotted"/>
{:else}
<Icon name="plus-square-dotted" />
{/if} {/if}
</Button> </Button>
<Tooltip <Tooltip

View File

@ -12,39 +12,37 @@
<script> <script>
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/svelte";
import { getContext, createEventDispatcher } from "svelte"; import { getContext } from "svelte";
import { Card, Spinner } from "@sveltestrap/sveltestrap"; import { Card, Spinner } from "@sveltestrap/sveltestrap";
import { maxScope, checkMetricDisabled } from "../utils.js"; import { maxScope, checkMetricDisabled } from "../utils.js";
import JobInfo from "./JobInfo.svelte"; import JobInfo from "./JobInfo.svelte";
import MetricPlot from "../plots/MetricPlot.svelte"; import MetricPlot from "../plots/MetricPlot.svelte";
import JobFootprint from "../helper/JobFootprint.svelte"; import JobFootprint from "../helper/JobFootprint.svelte";
export let job; /* Svelte 5 Props */
export let metrics; let {
export let plotWidth; triggerMetricRefresh = $bindable(false),
export let plotHeight = 275; job,
export let showFootprint; metrics,
export let triggerMetricRefresh = false; plotWidth,
export let previousSelect = false; plotHeight = 275,
showFootprint,
previousSelect = false,
selectJob,
unselectJob
} = $props();
const dispatch = createEventDispatcher(); /* Const Init */
const resampleConfig = getContext("resampling") || null; const client = getContextClient();
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0; const jobId = job.id;
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
let { id } = job; const scopes = (job.numNodes == 1)
let scopes = job.numNodes == 1 ? (job.numAcc >= 1)
? job.numAcc >= 1
? ["core", "accelerator"] ? ["core", "accelerator"]
: ["core"] : ["core"]
: ["node"]; : ["node"];
let selectedResolution = resampleDefault; const resampleConfig = getContext("resampling") || null;
let zoomStates = {}; const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
let thresholdStates = {};
$: isSelected = previousSelect || null;
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
const client = getContextClient();
const query = gql` const query = gql`
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) { query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) { jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) {
@ -77,52 +75,59 @@
} }
`; `;
function handleZoom(detail, metric) { /* State Init */
if ( // States have to differ, causes deathloop if just set let selectedResolution = $state(resampleDefault);
(zoomStates[metric]?.x?.min !== detail?.lastZoomState?.x?.min) && let zoomStates = $state({});
(zoomStates[metric]?.y?.max !== detail?.lastZoomState?.y?.max) let thresholdStates = $state({});
) {
zoomStates[metric] = {...detail.lastZoomState}
}
if ( // States have to differ, causes deathloop if just set /* Derived */
detail?.lastThreshold && let isSelected = $derived(previousSelect);
thresholdStates[metric] !== detail.lastThreshold let metricsQuery = $derived(queryStore({
) { // Handle to correctly reset on summed metric scope change
thresholdStates[metric] = detail.lastThreshold;
}
if (detail?.newRes) { // Triggers GQL
selectedResolution = detail.newRes
}
}
$: metricsQuery = queryStore({
client: client, client: client,
query: query, query: query,
variables: { id, metrics, scopes, selectedResolution }, variables: { id: jobId, metrics, scopes, selectedResolution },
})
);
/* Effects */
$effect(() => {
if (job.state === 'running' && triggerMetricRefresh === true) {
refreshMetrics();
}
}); });
$effect(() => {
if (isSelected == true && previousSelect == false) {
selectJob(jobId)
} else if (isSelected == false && previousSelect == true) {
unselectJob(jobId)
}
});
/* Functions */
function handleZoom(detail, metric) {
// Buffer last zoom state to allow seamless zoom on rerender
// console.log('Update zoomState for/with:', metric, {...detail.lastZoomState})
zoomStates[metric] = detail?.lastZoomState ? {...detail.lastZoomState} : null;
// Handle to correctly reset on summed metric scope change
// console.log('Update thresholdState for/with:', metric, detail.lastThreshold)
thresholdStates[metric] = detail?.lastThreshold ? detail.lastThreshold : null;
// Triggers GQL
if (detail?.newRes) {
// console.log('Update selectedResolution for/with:', metric, detail.newRes)
selectedResolution = detail.newRes;
}
}
function refreshMetrics() { function refreshMetrics() {
metricsQuery = queryStore({ metricsQuery = queryStore({
client: client, client: client,
query: query, query: query,
variables: { id, metrics, scopes, selectedResolution }, variables: { id: jobId, metrics, scopes, selectedResolution },
// requestPolicy: 'network-only' // use default cache-first for refresh // requestPolicy: 'network-only' // use default cache-first for refresh
}); });
} }
$: if (job.state === 'running' && triggerMetricRefresh === true) {
refreshMetrics();
}
$: if (isSelected == true && previousSelect == false) {
dispatch("select-job", job.id)
} else if (isSelected == false && previousSelect == true) {
dispatch("unselect-job", job.id)
}
// Helper
const selectScope = (jobMetrics) => const selectScope = (jobMetrics) =>
jobMetrics.reduce( jobMetrics.reduce(
(a, b) => (a, b) =>
@ -157,7 +162,6 @@
return jobMetric; return jobMetric;
} }
}); });
</script> </script>
<tr> <tr>
@ -196,7 +200,7 @@
<!-- 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.disabled == false && metric.data} {#if metric.disabled == false && metric.data}
<MetricPlot <MetricPlot
on:zoom={({detail}) => { handleZoom(detail, metric.data.name) }} on:zoom={({detail}) => handleZoom(detail, metric.data.name)}
height={plotHeight} height={plotHeight}
timestep={metric.data.metric.timestep} timestep={metric.data.metric.timestep}
scope={metric.data.scope} scope={metric.data.scope}

View File

@ -81,7 +81,7 @@
/* State Init */ /* State Init */
let requestedScopes = $state(presetScopes); let requestedScopes = $state(presetScopes);
let selectedResolution = $state(resampleConfig ? resampleDefault : 0); let selectedResolution = $state(resampleDefault);
let selectedHost = $state(null); let selectedHost = $state(null);
let zoomState = $state(null); let zoomState = $state(null);