From c4c422da5776bcae0d8927dfc4ddd95bd5684329 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 27 Jun 2025 15:52:54 +0200 Subject: [PATCH] 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);