Migrate job list view and filter components

- filters now inactive in user jobs, lists and analysis due to missing dispatch
This commit is contained in:
Christoph Kluge 2025-05-19 09:25:23 +02:00
parent 3f1768e467
commit ffd596e2c7
4 changed files with 99 additions and 80 deletions

View File

@ -8,7 +8,7 @@
--> -->
<script> <script>
import { onMount, getContext } from "svelte"; import { untrack, onMount, getContext } from "svelte";
import { import {
Row, Row,
Col, Col,
@ -28,42 +28,57 @@
import MetricSelection from "./generic/select/MetricSelection.svelte"; import MetricSelection from "./generic/select/MetricSelection.svelte";
const { query: initq } = init(); const { query: initq } = init();
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
export let filterPresets = {}; // Svelte 5 Props
export let authlevel; let { filterPresets, authlevel, roles } = $props();
export let roles;
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the // Svelte 5 Reactive Vars
let filterBuffer = []; let filterComponent = $state(); // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let selectedJobs = []; let selectedJobs = $state([]);
let jobList, let filterBuffer = $state([]);
jobCompare, let jobList = $state(null);
matchedListJobs, let jobCompare = $state(null);
matchedCompareJobs = null; let matchedListJobs = $state(0);
let sorting = { field: "startTime", type: "col", order: "DESC" }, let matchedCompareJobs = $state(0);
isSortingOpen = false, let isSortingOpen = $state(false);
isMetricsSelectionOpen = false; let showCompare = $state(false);
let metrics = filterPresets.cluster let isMetricsSelectionOpen = $state(false);
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
let metrics = $state(filterPresets.cluster
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
ccconfig.plot_list_selectedMetrics ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics; : ccconfig.plot_list_selectedMetrics
let showFootprint = filterPresets.cluster );
let showFootprint = $state(filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`] ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint; : !!ccconfig.plot_list_showFootprint
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null; );
// Classic Inits
let presetProject = filterPresets?.project ? filterPresets.project : "" let presetProject = filterPresets?.project ? filterPresets.project : ""
let showCompare = false;
// The filterPresets are handled by the Filters component, // The filterPresets are handled by the Filters component,
// so we need to wait for it to be ready before we can start a query. // so we need to wait for it to be ready before we can start a query.
// This is also why JobList component starts out with a paused query. // This is also why JobList component starts out with a paused query.
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
$: if (filterComponent && selectedJobs.length == 0) { function resetJobSelection() {
filterComponent.updateFilters({dbId: []}) if (filterComponent && selectedJobs.length === 0) {
} filterComponent.updateFilters({ dbId: [] });
};
};
$effect(() => {
// Reactive : Trigger Effect
selectedJobs.length
untrack(() => {
// Unreactive : Apply Reset w/o starting infinite loop
resetJobSelection()
});
});
</script> </script>
<!-- ROW1: Status--> <!-- ROW1: Status-->
@ -85,13 +100,13 @@
<Row cols={{ xs: 1, md: 2, lg: 5}} class="mb-3"> <Row cols={{ xs: 1, md: 2, lg: 5}} class="mb-3">
<Col lg="2" class="mb-2 mb-lg-0"> <Col lg="2" class="mb-2 mb-lg-0">
<ButtonGroup class="w-100"> <ButtonGroup class="w-100">
<Button outline color="primary" on:click={() => (isSortingOpen = true)} disabled={showCompare}> <Button outline color="primary" onclick={() => (isSortingOpen = true)} disabled={showCompare}>
<Icon name="sort-up" /> Sorting <Icon name="sort-up" /> Sorting
</Button> </Button>
<Button <Button
outline outline
color="primary" color="primary"
on:click={() => (isMetricsSelectionOpen = true)} onclick={() => (isMetricsSelectionOpen = true)}
> >
<Icon name="graph-up" /> Metrics <Icon name="graph-up" /> Metrics
</Button> </Button>
@ -99,11 +114,11 @@
</Col> </Col>
<Col lg="4" class="mb-1 mb-lg-0"> <Col lg="4" class="mb-1 mb-lg-0">
<Filters <Filters
showFilter={!showCompare}
{filterPresets}
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
bind:this={filterComponent} bind:this={filterComponent}
on:update-filters={({ detail }) => { {filterPresets}
showFilter={!showCompare}
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
applyFilters={(detail) => {
selectedCluster = detail.filters[0]?.cluster selectedCluster = detail.filters[0]?.cluster
? detail.filters[0].cluster.eq ? detail.filters[0].cluster.eq
: null; : null;
@ -122,13 +137,13 @@
{presetProject} {presetProject}
bind:authlevel bind:authlevel
bind:roles bind:roles
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)} setFilter={(filter) => filterComponent.updateFilters(filter)}
/> />
{/if} {/if}
</Col> </Col>
<Col lg="2" class="mb-1 mb-lg-0"> <Col lg="2" class="mb-1 mb-lg-0">
{#if !showCompare} {#if !showCompare}
<Refresher on:refresh={() => { <Refresher onRefresh={() => {
jobList.refreshJobs() jobList.refreshJobs()
jobList.refreshAllMetrics() jobList.refreshAllMetrics()
}} /> }} />
@ -136,7 +151,7 @@
</Col> </Col>
<Col lg="2" class="mb-2 mb-lg-0"> <Col lg="2" class="mb-2 mb-lg-0">
<ButtonGroup class="w-100"> <ButtonGroup class="w-100">
<Button color="primary" disabled={matchedListJobs >= 500 && !(selectedJobs.length != 0)} on:click={() => { <Button color="primary" disabled={matchedListJobs >= 500 && !(selectedJobs.length != 0)} onclick={() => {
if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs}, true) if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs}, true)
showCompare = !showCompare showCompare = !showCompare
}} > }} >
@ -144,7 +159,7 @@
'Compare Jobs' + (selectedJobs.length != 0 ? ` (${selectedJobs.length} selected)` : matchedListJobs >= 500 ? ` (Too Many)` : ``)} 'Compare Jobs' + (selectedJobs.length != 0 ? ` (${selectedJobs.length} selected)` : matchedListJobs >= 500 ? ` (Too Many)` : ``)}
</Button> </Button>
{#if !showCompare && selectedJobs.length != 0} {#if !showCompare && selectedJobs.length != 0}
<Button color="warning" on:click={() => { <Button color="warning" onclick={() => {
selectedJobs = [] // Only empty array, filters handled by reactive reset selectedJobs = [] // Only empty array, filters handled by reactive reset
}}> }}>
Clear Clear

View File

@ -16,7 +16,6 @@
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
@ -37,14 +36,16 @@
import Resources from "./filters/Resources.svelte"; import Resources from "./filters/Resources.svelte";
import Statistics from "./filters/Stats.svelte"; import Statistics from "./filters/Stats.svelte";
const dispatch = createEventDispatcher(); // Svelte 5 Props
let {
export let menuText = null; menuText = null,
export let filterPresets = {}; filterPresets = {},
export let disableClusterSelection = false; disableClusterSelection = false,
export let startTimeQuickSelect = false; startTimeQuickSelect = false,
export let matchedJobs = -2; matchedJobs = -2,
export let showFilter = true; showFilter = true,
applyFilters
} = $props();
const startTimeSelectOptions = [ const startTimeSelectOptions = [
{ range: "", rangeLabel: "No Selection"}, { range: "", rangeLabel: "No Selection"},
@ -92,7 +93,8 @@
stats: [], stats: [],
}; };
let filters = { // Svelte 5 Reactive Vars
let filters = $state({
projectMatch: filterPresets.projectMatch || "contains", projectMatch: filterPresets.projectMatch || "contains",
userMatch: filterPresets.userMatch || "contains", userMatch: filterPresets.userMatch || "contains",
jobIdMatch: filterPresets.jobIdMatch || "eq", jobIdMatch: filterPresets.jobIdMatch || "eq",
@ -126,19 +128,19 @@
numAccelerators: filterPresets.numAccelerators || { from: null, to: null }, numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
stats: filterPresets.stats || [], stats: filterPresets.stats || [],
}; });
let isClusterOpen = false, let isClusterOpen = $state(false)
isJobStatesOpen = false, let isJobStatesOpen = $state(false)
isStartTimeOpen = false, let isStartTimeOpen = $state(false)
isTagsOpen = false, let isTagsOpen = $state(false)
isDurationOpen = false, let isDurationOpen = $state(false)
isEnergyOpen = false, let isEnergyOpen = $state(false)
isResourcesOpen = false, let isResourcesOpen = $state(false)
isStatsOpen = false, let isStatsOpen = $state(false)
isNodesModified = false, let isNodesModified = $state(false)
isHwthreadsModified = false, let isHwthreadsModified = $state(false)
isAccsModified = false; let isAccsModified = $state(false)
// Can be called from the outside to trigger a 'update' event from this component. // Can be called from the outside to trigger a 'update' event from this component.
// 'force' option empties existing filters and then applies only 'additionalFilters' // 'force' option empties existing filters and then applies only 'additionalFilters'
@ -217,7 +219,7 @@
if (filters.stats.length != 0) if (filters.stats.length != 0)
items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) }); items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) });
dispatch("update-filters", { filters: items }); applyFilters({ filters: items });
changeURL(); changeURL();
return items; return items;
} }
@ -296,7 +298,7 @@
<!-- Dropdown-Button --> <!-- Dropdown-Button -->
<ButtonGroup> <ButtonGroup>
{#if showFilter} {#if showFilter}
<ButtonDropdown class="cc-dropdown-on-hover mb-1" style="{(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}"> <ButtonDropdown class="cc-dropdown-on-hover mb-1" style={(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}>
<DropdownToggle outline caret color="success"> <DropdownToggle outline caret color="success">
<Icon name="sliders" /> <Icon name="sliders" />
Filters Filters

View File

@ -8,22 +8,21 @@
- `refresh`: When fired, the upstream component refreshes its contents - `refresh`: When fired, the upstream component refreshes its contents
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap"; import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher(); let refreshInterval = $state(null);
let refreshInterval = null;
let refreshIntervalId = null; let refreshIntervalId = null;
function refreshIntervalChanged() { function refreshIntervalChanged() {
if (refreshIntervalId != null) clearInterval(refreshIntervalId); if (refreshIntervalId != null) clearInterval(refreshIntervalId);
if (refreshInterval == null) return; if (refreshInterval == null) return;
refreshIntervalId = setInterval(() => dispatch("refresh"), refreshInterval); refreshIntervalId = setInterval(() => dispatch("refresh"), refreshInterval);
} }
export let initially = null; let {
initially = null,
onRefresh
} = $props();
if (initially != null) { if (initially != null) {
refreshInterval = initially * 1000; refreshInterval = initially * 1000;
@ -36,7 +35,7 @@
type="select" type="select"
title="Periodic refresh interval" title="Periodic refresh interval"
bind:value={refreshInterval} bind:value={refreshInterval}
on:change={refreshIntervalChanged} onchange={refreshIntervalChanged}
> >
<option value={null}>No Interval</option> <option value={null}>No Interval</option>
<option value={30 * 1000}>30 Seconds</option> <option value={30 * 1000}>30 Seconds</option>
@ -46,7 +45,7 @@
</Input> </Input>
<Button <Button
outline outline
on:click={() => dispatch("refresh")} onclick={() => onRefresh()}
disabled={refreshInterval != null} disabled={refreshInterval != null}
> >
<Icon name="arrow-clockwise" /> Refresh <Icon name="arrow-clockwise" /> Refresh

View File

@ -12,16 +12,19 @@
<script> <script>
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap"; import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import { scramble, scrambleNames } from "../utils.js"; import { scramble, scrambleNames } from "../utils.js";
const dispatch = createEventDispatcher(); // If page with this component has project preset, keep preset until reset
let {
presetProject = "",
authlevel = null,
roles = null,
setFilter
} = $props();
let mode = $state(presetProject ? "jobName" : "project");
let term = $state("");
export let presetProject = ""; // If page with this component has project preset, keep preset until reset
export let authlevel = null;
export let roles = null;
let mode = presetProject ? "jobName" : "project";
let term = "";
let user = ""; let user = "";
let project = presetProject ? presetProject : ""; let project = presetProject ? presetProject : "";
let jobName = ""; let jobName = "";
@ -52,7 +55,7 @@
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
dispatch("set-filter", { setFilter({
user, user,
project, project,
jobName jobName
@ -65,7 +68,7 @@
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
dispatch("set-filter", { setFilter({
project, project,
jobName jobName
}); });
@ -91,7 +94,7 @@
class="form-select" class="form-select"
title="Search Mode" title="Search Mode"
bind:value={mode} bind:value={mode}
on:change={modeChanged} onchange={modeChanged}
> >
{#if !presetProject} {#if !presetProject}
<option value={"project"}>Project</option> <option value={"project"}>Project</option>
@ -104,12 +107,12 @@
<Input <Input
type="text" type="text"
bind:value={term} bind:value={term}
on:change={() => termChanged()} onchange={() => termChanged()}
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)} onkeyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
placeholder={presetProject ? `Find ${mode} in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Find ${mode} ...`} placeholder={presetProject ? `Find ${mode} in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Find ${mode} ...`}
/> />
{#if presetProject} {#if presetProject}
<Button title="Reset Project" on:click={() => resetProject()} <Button title="Reset Project" onclick={() => resetProject()}
><Icon name="arrow-counterclockwise" /></Button ><Icon name="arrow-counterclockwise" /></Button
> >
{/if} {/if}