From 79a6c9e90d660856e0b518833ede75f7b40ab0da Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Wed, 25 Jun 2025 17:41:11 +0200 Subject: [PATCH 1/8] Migrate Job View --- web/frontend/src/Job.root.svelte | 202 +++++++++++++++++-------------- 1 file changed, 109 insertions(+), 93 deletions(-) diff --git a/web/frontend/src/Job.root.svelte b/web/frontend/src/Job.root.svelte index 427c9ae..fd49243 100644 --- a/web/frontend/src/Job.root.svelte +++ b/web/frontend/src/Job.root.svelte @@ -9,10 +9,11 @@ --> {#each items as item} - + + {@render gridContent(item)} {/each} diff --git a/web/frontend/src/generic/plots/Scatter.svelte b/web/frontend/src/generic/plots/Scatter.svelte index 514223b..76c1a17 100644 --- a/web/frontend/src/generic/plots/Scatter.svelte +++ b/web/frontend/src/generic/plots/Scatter.svelte @@ -182,6 +182,6 @@ -
- +
+
From e94b2505414e78d0f6c464b821082430cda25384 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Thu, 26 Jun 2025 12:29:48 +0200 Subject: [PATCH 3/8] Migrate nodeList subcomponents --- web/frontend/src/generic/PlotGrid.svelte | 22 ++++----- .../generic/select/DoubleRangeSlider.svelte | 5 +++ .../src/systems/nodelist/NodeInfo.svelte | 35 +++++++++------ .../src/systems/nodelist/NodeListRow.svelte | 45 ++++++++++--------- 4 files changed, 62 insertions(+), 45 deletions(-) diff --git a/web/frontend/src/generic/PlotGrid.svelte b/web/frontend/src/generic/PlotGrid.svelte index 213041e..3647344 100644 --- a/web/frontend/src/generic/PlotGrid.svelte +++ b/web/frontend/src/generic/PlotGrid.svelte @@ -6,18 +6,18 @@ - `items [Any]`: List of plot components to render --> - diff --git a/web/frontend/src/generic/select/DoubleRangeSlider.svelte b/web/frontend/src/generic/select/DoubleRangeSlider.svelte index 0022673..7554bdf 100644 --- a/web/frontend/src/generic/select/DoubleRangeSlider.svelte +++ b/web/frontend/src/generic/select/DoubleRangeSlider.svelte @@ -18,6 +18,7 @@ Changes #2: Rewritten for Svelte 5, removed bodyHandler --> diff --git a/web/frontend/src/systems/nodelist/NodeListRow.svelte b/web/frontend/src/systems/nodelist/NodeListRow.svelte index 7adb282..571ca63 100644 --- a/web/frontend/src/systems/nodelist/NodeListRow.svelte +++ b/web/frontend/src/systems/nodelist/NodeListRow.svelte @@ -18,10 +18,14 @@ import MetricPlot from "../../generic/plots/MetricPlot.svelte"; import NodeInfo from "./NodeInfo.svelte"; - export let cluster; - export let nodeData; - export let selectedMetrics; + /* Svelte 5 Props */ + let { + cluster, + nodeData, + selectedMetrics, + } = $props(); + /* Const Init */ const client = getContextClient(); const paging = { itemsPerPage: 50, page: 1 }; const sorting = { field: "startTime", type: "col", order: "DESC" }; @@ -30,7 +34,6 @@ { node: { contains: nodeData.host } }, { state: ["running"] }, ]; - const nodeJobsQuery = gql` query ( $filter: [JobFilter!]! @@ -53,13 +56,19 @@ } `; - $: nodeJobsData = queryStore({ - client: client, - query: nodeJobsQuery, - variables: { paging, sorting, filter }, - }); + /* Derived */ + const nodeJobsData = $derived(queryStore({ + client: client, + query: nodeJobsQuery, + variables: { paging, sorting, filter }, + }) + ); - // Helper + let extendedLegendData = $derived($nodeJobsData?.data ? buildExtendedLegend() : null); + let refinedData = $derived(nodeData?.metrics ? sortAndSelectScope(nodeData.metrics) : null); + let dataHealth = $derived(refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0))); + + /* Functions */ const selectScope = (nodeMetrics) => nodeMetrics.reduce( (a, b) => @@ -89,15 +98,8 @@ } }); - let refinedData; - let dataHealth; - $: if (nodeData?.metrics) { - refinedData = sortAndSelectScope(nodeData?.metrics) - dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0)) - } - - let extendedLegendData = null; - $: if ($nodeJobsData?.data) { + function buildExtendedLegend() { + let pendingExtendedLegendData = null // Build Extended for allocated nodes [Commented: Only Build extended Legend For Shared Nodes] if ($nodeJobsData.data.jobs.count >= 1) { // "&& !$nodeJobsData.data.jobs.items[0].exclusive)" const accSet = Array.from(new Set($nodeJobsData.data.jobs.items @@ -107,11 +109,11 @@ ) )).flat(2) - extendedLegendData = {} + pendingExtendedLegendData = {}; for (const accId of accSet) { const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId))) const matchUser = matchJob?.user ? matchJob.user : null - extendedLegendData[accId] = { + pendingExtendedLegendData[accId] = { user: (scrambleNames && matchUser) ? scramble(matchUser) : (matchUser ? matchUser : '-'), @@ -120,6 +122,7 @@ } // Theoretically extendable for hwthreadIDs } + return pendingExtendedLegendData; } From c3a6126799b5f072084d5553bae4417296e826e0 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Thu, 26 Jun 2025 18:41:27 +0200 Subject: [PATCH 4/8] Migrate and rework job view metricplot wrapper --- web/frontend/src/Job.root.svelte | 21 +- .../src/generic/plots/MetricPlot.svelte | 5 +- web/frontend/src/job/Metric.svelte | 195 ++++++++---------- 3 files changed, 88 insertions(+), 133 deletions(-) diff --git a/web/frontend/src/Job.root.svelte b/web/frontend/src/Job.root.svelte index 5ece4fc..948519d 100644 --- a/web/frontend/src/Job.root.svelte +++ b/web/frontend/src/Job.root.svelte @@ -75,26 +75,8 @@ name scope metric { - unit { - prefix - base - } - timestep - statisticsSeries { - min - mean - median - max - } series { hostname - id - data - statistics { - min - avg - max - } } } } @@ -343,8 +325,7 @@ metricName={item.metric} metricUnit={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.unit} nativeScope={$initq.data.globalMetrics.find((gm) => gm.name == item.metric)?.scope} - rawData={item.data.map((x) => x.metric)} - scopes={item.data.map((x) => x.scope)} + presetScopes={item.data.map((x) => x.scope)} isShared={$initq.data.job.exclusive != 1} /> {:else if item.disabled == true} diff --git a/web/frontend/src/generic/plots/MetricPlot.svelte b/web/frontend/src/generic/plots/MetricPlot.svelte index 56e819d..50bb3b1 100644 --- a/web/frontend/src/generic/plots/MetricPlot.svelte +++ b/web/frontend/src/generic/plots/MetricPlot.svelte @@ -440,7 +440,7 @@ /* IF Zoom Enabled */ if (resampleConfig) { u.over.addEventListener("dblclick", (e) => { - // console.log('Dispatch Reset') + // console.log('Dispatch: Zoom Reset') dispatch('zoom', { lastZoomState: { x: { time: false }, @@ -506,7 +506,7 @@ }); // Prevents non-required dispatches if (timestep !== closest) { - // console.log('Dispatch Zoom with Res from / to', timestep, closest) + // console.log('Dispatch: Zoom with Res from / to', timestep, closest) dispatch('zoom', { newRes: closest, lastZoomState: u?.scales, @@ -514,6 +514,7 @@ }); } } else { + // console.log('Dispatch: Zoom Update States') dispatch('zoom', { lastZoomState: u?.scales, lastThreshold: thresholds?.normal diff --git a/web/frontend/src/job/Metric.svelte b/web/frontend/src/job/Metric.svelte index 63a9b80..7040e9e 100644 --- a/web/frontend/src/job/Metric.svelte +++ b/web/frontend/src/job/Metric.svelte @@ -31,33 +31,22 @@ } from "../generic/utils.js"; import Timeseries from "../generic/plots/MetricPlot.svelte"; - export let job; - export let metricName; - export let metricUnit; - export let nativeScope; - export let scopes; - export let rawData; - export let isShared = false; + /* Svelte 5 Props */ + let { + job, + metricName, + metricUnit, + nativeScope, + presetScopes, + isShared = false, + } = $props(); + /* Const Init */ + const client = getContextClient(); + const statsPattern = /(.*)-stat$/; const resampleConfig = getContext("resampling") || null; const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0; - - let selectedHost = null; - let error = null; - let selectedScope = minScope(scopes); - let selectedResolution = null; - let pendingResolution = resampleDefault; - let selectedScopeIndex = scopes.findIndex((s) => s == minScope(scopes)); - let patternMatches = false; - let nodeOnly = false; // If, after load-all, still only node scope returned - let statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null); - let zoomState = null; - let pendingZoomState = null; - let thresholdState = null; - - const statsPattern = /(.*)-stat$/; const unit = (metricUnit?.prefix ? metricUnit.prefix : "") + (metricUnit?.base ? metricUnit.base : ""); - const client = getContextClient(); const subQuery = gql` query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) { singleUpdate: jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) { @@ -90,84 +79,68 @@ } `; - function handleZoom(detail) { - if ( // States have to differ, causes deathloop if just set - (pendingZoomState?.x?.min !== detail?.lastZoomState?.x?.min) && - (pendingZoomState?.y?.max !== detail?.lastZoomState?.y?.max) - ) { - pendingZoomState = {...detail.lastZoomState}; - } + /* State Init */ + let requestedScopes = $state(presetScopes); + let selectedResolution = $state(resampleConfig ? resampleDefault : 0); - if (detail?.lastThreshold) { // Handle to correctly reset on summed metric scope change - thresholdState = detail.lastThreshold; - } else { - thresholdState = null; - } + let selectedHost = $state(null); + let zoomState = $state(null); + let thresholdState = $state(null); - if (detail?.newRes) { // Triggers GQL - pendingResolution = detail.newRes; - } - }; - - let metricData; - let selectedScopes = [...scopes]; - const dbid = job.id; - const selectedMetrics = [metricName]; - - $: if (selectedScope || pendingResolution) { - - if (resampleConfig && !selectedResolution) { - // Skips reactive data load on init || Only if resampling is enabled - selectedResolution = Number(pendingResolution) - - } else { - if (selectedScope == "load-all") { - selectedScopes = [...scopes, "socket", "core", "accelerator"] - } - - if (resampleConfig && pendingResolution) { - selectedResolution = Number(pendingResolution) - } - - metricData = queryStore({ - client: client, - query: subQuery, - variables: { dbid, selectedMetrics, selectedScopes, selectedResolution: (resampleConfig ? selectedResolution : 0) }, - // Never user network-only: causes reactive load-loop! - }); - - if ($metricData && !$metricData.fetching) { - rawData = $metricData.data.singleUpdate.map((x) => x.metric) - scopes = $metricData.data.singleUpdate.map((x) => x.scope) - statsSeries = rawData.map((data) => data?.statisticsSeries ? data.statisticsSeries : null) - - // Keep Zoomlevel if ResChange By Zoom - if (pendingZoomState) { - zoomState = {...pendingZoomState} - } - - // On additional scope request - if (selectedScope == "load-all") { - // Set selected scope to min of returned scopes - selectedScope = minScope(scopes) - nodeOnly = (selectedScope == "node") // "node" still only scope after load-all - } - - patternMatches = statsPattern.exec(selectedScope) - - if (!patternMatches) { - selectedScopeIndex = scopes.findIndex((s) => s == selectedScope); - } else { - selectedScopeIndex = scopes.findIndex((s) => s == patternMatches[1]); - } - } - } - } - - $: data = rawData[selectedScopeIndex]; - $: series = data?.series?.filter( - (series) => selectedHost == null || series.hostname == selectedHost, + /* Derived */ + const metricData = $derived(queryStore({ + client: client, + query: subQuery, + variables: { + dbid: job.id, + selectedMetrics: [metricName], + selectedScopes: [...requestedScopes], + selectedResolution + }, + // Never user network-only: causes reactive load-loop! + }) ); + + const rawData = $derived($metricData?.data ? $metricData.data.singleUpdate.map((x) => x.metric) : []); + const availableScopes = $derived($metricData?.data ? $metricData.data.singleUpdate.map((x) => x.scope) : presetScopes); + let selectedScope = $derived(minScope(availableScopes)); + const patternMatches = $derived(statsPattern.exec(selectedScope)); + const selectedScopeIndex = $derived.by(() => { + if (!patternMatches) { + return availableScopes.findIndex((s) => s == selectedScope); + } else { + return availableScopes.findIndex((s) => s == patternMatches[1]); + } + }); + const selectedData = $derived(rawData[selectedScopeIndex]); + const selectedSeries = $derived(rawData[selectedScopeIndex]?.series?.filter( + (series) => selectedHost == null || series.hostname == selectedHost + ) + ); + const statsSeries = $derived(rawData.map((rd) => rd?.statisticsSeries ? rd.statisticsSeries : null)); + + /* Effect */ + $effect(() => { + // Only triggered once + if (selectedScope == "load-all") { + requestedScopes = ["node", "socket", "core", "accelerator"]; + } + }); + + /* Functions */ + function handleZoom(detail) { + // Buffer last zoom state to allow seamless zoom on rerender + // console.log('Update zoomState with:', {...detail.lastZoomState}) + zoomState = detail?.lastZoomState ? {...detail.lastZoomState} : null; + // Handle to correctly reset on summed metric scope change + // console.log('Update thresholdState with:', detail.lastThreshold) + thresholdState = detail?.lastThreshold ? detail.lastThreshold : null; + // Triggers GQL + if (detail?.newRes) { + // console.log('Update selectedResolution with:', detail.newRes) + selectedResolution = detail.newRes; + } + }; @@ -175,13 +148,13 @@ {metricName} ({unit}) @@ -194,37 +167,37 @@ {/if} -{#key series} - {#if $metricData?.fetching == true} +{#key selectedSeries} + {#if $metricData.fetching} - {:else if error != null} - {error.message} - {:else if series != null && !patternMatches} + {:else if $metricData.error} + {$metricData.error.message} + {:else if selectedSeries != null && !patternMatches} { handleZoom(detail) }} + on:zoom={({detail}) => handleZoom(detail)} cluster={job.cluster} subCluster={job.subCluster} - timestep={data.timestep} + timestep={selectedData.timestep} scope={selectedScope} metric={metricName} numaccs={job.numAcc} numhwthreads={job.numHWThreads} - {series} + series={selectedSeries} {isShared} {zoomState} {thresholdState} /> {:else if statsSeries[selectedScopeIndex] != null && patternMatches} { handleZoom(detail) }} + on:zoom={({detail}) => handleZoom(detail)} cluster={job.cluster} subCluster={job.subCluster} - timestep={data.timestep} + timestep={selectedData.timestep} scope={selectedScope} metric={metricName} numaccs={job.numAcc} numhwthreads={job.numHWThreads} - {series} + series={selectedSeries} {isShared} {zoomState} {thresholdState} From 47843b208731ea1f49cb331e88675e37dc88fa5c Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 27 Jun 2025 11:15:17 +0200 Subject: [PATCH 5/8] Optimize jobview gql query load --- web/frontend/src/Job.root.svelte | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/web/frontend/src/Job.root.svelte b/web/frontend/src/Job.root.svelte index 948519d..dadb781 100644 --- a/web/frontend/src/Job.root.svelte +++ b/web/frontend/src/Job.root.svelte @@ -69,15 +69,14 @@ `); const client = getContextClient(); const ccconfig = getContext("cc-config"); + /* Note: Actual metric data queried in Component, only require base infos here -> reduce backend load by requesting just stats */ const query = gql` query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) { - jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) { + scopedJobStats(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes) { name scope - metric { - series { - hostname - } + stats { + hostname } } } @@ -101,7 +100,7 @@ const missingMetrics = $derived.by(() => { if ($initq?.data && $jobMetrics?.data) { let job = $initq.data.job; - let metrics = $jobMetrics.data.jobMetrics; + let metrics = $jobMetrics.data.scopedJobStats; let metricNames = $initq.data.globalMetrics.reduce((names, gm) => { if (gm.availability.find((av) => av.cluster === job.cluster)) { names.push(gm.name); @@ -127,7 +126,7 @@ const missingHosts = $derived.by(() => { if ($initq?.data && $jobMetrics?.data) { let job = $initq.data.job; - let metrics = $jobMetrics.data.jobMetrics; + let metrics = $jobMetrics.data.scopedJobStats; let metricNames = $initq.data.globalMetrics.reduce((names, gm) => { if (gm.availability.find((av) => av.cluster === job.cluster)) { names.push(gm.name); @@ -143,7 +142,7 @@ !metrics.some( (jm) => jm.scope == "node" && - jm.metric.series.some((series) => series.hostname == hostname), + jm.stats.some((s) => s.hostname == hostname), ), ), })) @@ -315,7 +314,7 @@ - {:else if $initq?.data && $jobMetrics?.data?.jobMetrics} + {:else if $initq?.data && $jobMetrics?.data?.scopedJobStats} {#snippet gridContent(item)} {#if item.data} @@ -352,7 +351,7 @@ Date: Fri, 27 Jun 2025 15:52:54 +0200 Subject: [PATCH 6/8] Migrate jobList and jobListRow --- web/frontend/src/Analysis.root.svelte | 1 + web/frontend/src/Job.root.svelte | 2 +- web/frontend/src/Jobs.root.svelte | 11 +- web/frontend/src/Node.root.svelte | 1 + web/frontend/src/Status.root.svelte | 1 + web/frontend/src/User.root.svelte | 9 +- web/frontend/src/generic/JobCompare.svelte | 2 +- web/frontend/src/generic/JobList.svelte | 238 ++++++++++-------- .../src/generic/joblist/JobInfo.svelte | 8 +- .../src/generic/joblist/JobListRow.svelte | 118 ++++----- web/frontend/src/job/Metric.svelte | 2 +- 11 files changed, 208 insertions(+), 185 deletions(-) diff --git a/web/frontend/src/Analysis.root.svelte b/web/frontend/src/Analysis.root.svelte index 0237174..afad314 100644 --- a/web/frontend/src/Analysis.root.svelte +++ b/web/frontend/src/Analysis.root.svelte @@ -562,6 +562,7 @@ + {#snippet histoGridContent(item)} {:else if $initq?.data && $jobMetrics?.data?.scopedJobStats} - + {#snippet gridContent(item)} {#if item.data} {:else} {/if} @@ -201,7 +201,8 @@ presetSorting={sorting} applySorting={(newSort) => sorting = {...newSort} - }/> + } +/> {:else} + {#snippet gridContent(item)}

{item.name} diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte index d7ac25b..7e7fc0c 100644 --- a/web/frontend/src/Status.root.svelte +++ b/web/frontend/src/Status.root.svelte @@ -676,6 +676,7 @@ {#if selectedHistograms} + {#snippet gridContent(item)} {:else}
+ {#snippet gridContent(item)} @@ -390,10 +391,10 @@ presetMetrics={metrics} cluster={selectedCluster} configName="plot_list_selectedMetrics" + footprintSelect applyMetrics={(newMetrics) => metrics = [...newMetrics] } - footprintSelect /> { - return JSON.stringify(a) === JSON.stringify(b); - } - - export let sorting = { field: "startTime", type: "col", order: "DESC" }; - 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 : "") - } + /* Svelte 5 Props */ + let { + matchedListJobs = $bindable(0), + selectedJobs = $bindable([]), + metrics = getContext("cc-config").plot_list_selectedMetrics, + sorting = { field: "startTime", type: "col", order: "DESC" }, + showFootprint = false, + filterBuffer = [], + } = $props(); + /* 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 query = gql` query ( @@ -107,50 +94,102 @@ } `; - $: jobsStore = queryStore({ - client: client, - query: query, - variables: { paging, sorting, filter }, + /* 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, + query: query, + variables: { paging, sorting, filter }, + }) + ); + + /* Effects */ + $effect(() => { + if ($jobsStore?.data) { + matchedListJobs = $jobsStore.data.jobs.count; + } else { + matchedListJobs = -1 + } }); - let jobs = []; - $: if ($initialized && $jobsStore.data) { - if (usePaging) { - jobs = [...$jobsStore.data.jobs.items] - } else { // Prevents jump to table head in continiuous mode, only if no change in sort or filter - if (equalsCheck(filter, lastFilter) && equalsCheck(sorting, lastSorting)) { - // console.log('Both Equal: Continuous Addition ... Set None') - jobs = jobs.concat([...$jobsStore.data.jobs.items]) - } else if (equalsCheck(filter, lastFilter)) { - // console.log('Filter Equal: Continuous Reset ... Set lastSorting') - lastSorting = { ...sorting } - jobs = [...$jobsStore.data.jobs.items] - } else if (equalsCheck(sorting, lastSorting)) { - // console.log('Sorting Equal: Continuous Reset ... Set lastFilter') - lastFilter = [ ...filter ] - jobs = [...$jobsStore.data.jobs.items] - } else { - // console.log('None Equal: Continuous Reset ... Set lastBoth') - lastSorting = { ...sorting } - lastFilter = [ ...filter ] - jobs = [...$jobsStore.data.jobs.items] - } + $effect(() => { + 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; } - } + }); - $: 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; + $effect(() => { + if ($initialized && $jobsStore?.data) { + if (usePaging) { + jobs = [...$jobsStore.data.jobs.items] + } else { // Prevents jump to table head in continiuous mode, only if no change in sort or filter + if (equalsCheck(filter, lastFilter) && equalsCheck(sorting, lastSorting)) { + // console.log('Both Equal: Continuous Addition ... Set None') + jobs = jobs.concat([...$jobsStore.data.jobs.items]) + } else if (equalsCheck(filter, lastFilter)) { + // console.log('Filter Equal: Continuous Reset ... Set lastSorting') + lastSorting = { ...sorting } + jobs = [...$jobsStore.data.jobs.items] + } else if (equalsCheck(sorting, lastSorting)) { + // console.log('Sorting Equal: Continuous Reset ... Set lastFilter') + lastFilter = [ ...filter ] + jobs = [...$jobsStore.data.jobs.items] + } else { + // console.log('None Equal: Continuous Reset ... Set lastBoth') + lastSorting = { ...sorting } + lastFilter = [ ...filter ] + jobs = [...$jobsStore.data.jobs.items] + } + } + }; + }); + /* Functions */ // Force refresh list with existing unchanged variables (== usually would not trigger reactivity) export function refreshJobs() { if (!usePaging) { jobs = []; // Empty Joblist before refresh, prevents infinite buildup - paging = { itemsPerPage: 10, page: 1 }; + page = 1; } jobsStore = queryStore({ client: client, @@ -178,8 +217,26 @@ filter = filters; } 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 }) => { return mutationStore({ @@ -193,52 +250,11 @@ }); }; - function updateConfiguration(value, page) { - updateConfigurationMutation({ - 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; - } - }); + const equalsCheck = (a, b) => { + return JSON.stringify(a) === JSON.stringify(b); } - 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 != 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; + /* Init Header */ stickyHeader( ".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)", (x) => (headerPaddingTop = x), @@ -292,8 +308,8 @@ {:else} {#each jobs as job (job.id)} selectedJobs = [...selectedJobs, detail]} - on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)} + selectJob={(detail) => selectedJobs = [...selectedJobs, detail]} + unselectJob={(detail) => selectedJobs = selectedJobs.filter(item => item !== detail)} /> {:else} diff --git a/web/frontend/src/generic/joblist/JobInfo.svelte b/web/frontend/src/generic/joblist/JobInfo.svelte index 5886c61..bb8b90b 100644 --- a/web/frontend/src/generic/joblist/JobInfo.svelte +++ b/web/frontend/src/generic/joblist/JobInfo.svelte @@ -20,7 +20,7 @@ username = null, authlevel= null, roles = null, - isSelected = null, + isSelected = $bindable(), showSelect = false, } = $props(); @@ -89,10 +89,8 @@ }}> {#if isSelected} - {:else if isSelected == false} - - {:else} - + {:else } + {/if} import { queryStore, gql, getContextClient } from "@urql/svelte"; - import { getContext, createEventDispatcher } from "svelte"; + import { getContext } from "svelte"; import { Card, Spinner } from "@sveltestrap/sveltestrap"; import { maxScope, checkMetricDisabled } from "../utils.js"; import JobInfo from "./JobInfo.svelte"; import MetricPlot from "../plots/MetricPlot.svelte"; import JobFootprint from "../helper/JobFootprint.svelte"; - export let job; - export let metrics; - export let plotWidth; - export let plotHeight = 275; - export let showFootprint; - export let triggerMetricRefresh = false; - export let previousSelect = false; + /* Svelte 5 Props */ + let { + triggerMetricRefresh = $bindable(false), + job, + metrics, + plotWidth, + plotHeight = 275, + showFootprint, + previousSelect = false, + selectJob, + unselectJob + } = $props(); - const dispatch = createEventDispatcher(); - const resampleConfig = getContext("resampling") || null; - const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0; - - let { id } = job; - let scopes = job.numNodes == 1 - ? job.numAcc >= 1 + /* Const Init */ + const client = getContextClient(); + const jobId = job.id; + const cluster = getContext("clusters").find((c) => c.name == job.cluster); + const scopes = (job.numNodes == 1) + ? (job.numAcc >= 1) ? ["core", "accelerator"] : ["core"] : ["node"]; - let selectedResolution = resampleDefault; - let zoomStates = {}; - let thresholdStates = {}; - - $: isSelected = previousSelect || null; - - const cluster = getContext("clusters").find((c) => c.name == job.cluster); - const client = getContextClient(); + const resampleConfig = getContext("resampling") || null; + const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0; const query = gql` query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) { jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) { @@ -77,52 +75,59 @@ } `; - function handleZoom(detail, metric) { - if ( // States have to differ, causes deathloop if just set - (zoomStates[metric]?.x?.min !== detail?.lastZoomState?.x?.min) && - (zoomStates[metric]?.y?.max !== detail?.lastZoomState?.y?.max) - ) { - zoomStates[metric] = {...detail.lastZoomState} + /* State Init */ + let selectedResolution = $state(resampleDefault); + let zoomStates = $state({}); + let thresholdStates = $state({}); + + /* Derived */ + let isSelected = $derived(previousSelect); + let metricsQuery = $derived(queryStore({ + client: client, + query: query, + variables: { id: jobId, metrics, scopes, selectedResolution }, + }) + ); + + /* Effects */ + $effect(() => { + if (job.state === 'running' && triggerMetricRefresh === true) { + refreshMetrics(); } + }); - if ( // States have to differ, causes deathloop if just set - detail?.lastThreshold && - thresholdStates[metric] !== detail.lastThreshold - ) { // Handle to correctly reset on summed metric scope change - thresholdStates[metric] = detail.lastThreshold; - } + $effect(() => { + if (isSelected == true && previousSelect == false) { + selectJob(jobId) + } else if (isSelected == false && previousSelect == true) { + unselectJob(jobId) + } + }); - if (detail?.newRes) { // Triggers GQL - selectedResolution = detail.newRes + /* 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; } } - $: metricsQuery = queryStore({ - client: client, - query: query, - variables: { id, metrics, scopes, selectedResolution }, - }); - function refreshMetrics() { metricsQuery = queryStore({ client: client, query: query, - variables: { id, metrics, scopes, selectedResolution }, + variables: { id: jobId, metrics, scopes, selectedResolution }, // 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) => jobMetrics.reduce( (a, b) => @@ -157,7 +162,6 @@ return jobMetric; } }); - @@ -196,7 +200,7 @@ {#if metric.disabled == false && metric.data} { handleZoom(detail, metric.data.name) }} + on:zoom={({detail}) => handleZoom(detail, metric.data.name)} height={plotHeight} timestep={metric.data.metric.timestep} scope={metric.data.scope} diff --git a/web/frontend/src/job/Metric.svelte b/web/frontend/src/job/Metric.svelte index 7040e9e..eba4102 100644 --- a/web/frontend/src/job/Metric.svelte +++ b/web/frontend/src/job/Metric.svelte @@ -81,7 +81,7 @@ /* State Init */ let requestedScopes = $state(presetScopes); - let selectedResolution = $state(resampleConfig ? resampleDefault : 0); + let selectedResolution = $state(resampleDefault); let selectedHost = $state(null); let zoomState = $state(null); From b8c30b5703dc36ac468a21353a7e54ba6d2babf3 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 27 Jun 2025 18:42:18 +0200 Subject: [PATCH 7/8] Fix continuous scroll in sv5 joblist, rework joblist logic --- web/frontend/src/generic/JobList.svelte | 102 ++++++++---------- .../src/generic/joblist/JobListRow.svelte | 9 +- web/frontend/src/systems/NodeList.svelte | 2 +- 3 files changed, 49 insertions(+), 64 deletions(-) diff --git a/web/frontend/src/generic/JobList.svelte b/web/frontend/src/generic/JobList.svelte index 20a9e8c..9b9e308 100644 --- a/web/frontend/src/generic/JobList.svelte +++ b/web/frontend/src/generic/JobList.svelte @@ -14,7 +14,7 @@ -->