Migrate select components and adapt parents

This commit is contained in:
Christoph Kluge 2025-06-18 18:14:56 +02:00
parent 6a6dca3fce
commit 1e039cb1bf
9 changed files with 232 additions and 139 deletions

View File

@ -196,7 +196,12 @@
</Col> </Col>
</Row> </Row>
<Sorting bind:sorting bind:isOpen={isSortingOpen}/> <Sorting
bind:isOpen={isSortingOpen}
presetSorting={sorting}
applySorting={(newSort) =>
sorting = {...newSort}
}/>
<MetricSelection <MetricSelection
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}

View File

@ -146,7 +146,14 @@
</Col> </Col>
<!-- Time Col --> <!-- Time Col -->
<Col> <Col>
<TimeSelection bind:from bind:to /> <TimeSelection
presetFrom={from}
presetTo={to}
applyTime={(newFrom, newTo) => {
from = newFrom;
to = newTo;
}}
/>
</Col> </Col>
<!-- Concurrent Col --> <!-- Concurrent Col -->
<Col class="mt-2 mt-lg-0"> <Col class="mt-2 mt-lg-0">

View File

@ -697,7 +697,10 @@
{/if} {/if}
<HistogramSelection <HistogramSelection
bind:cluster {cluster}
bind:selectedHistograms
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms}
applyChange={(newSelection) => {
selectedHistograms = [...newSelection];
}}
/> />

View File

@ -56,7 +56,7 @@
/* State Init */ /* State Init */
let to = $state(toPreset || new Date(Date.now())); let to = $state(toPreset || new Date(Date.now()));
let from = $state(fromPreset || new Date(nowDate.setHours(nowDate.getHours() - 12))); let from = $state(fromPreset || new Date(nowDate.setHours(nowDate.getHours() - 4)));
let selectedResolution = $state(resampleConfig ? resampleDefault : 0); let selectedResolution = $state(resampleConfig ? resampleDefault : 0);
let hostnameFilter = $state(""); let hostnameFilter = $state("");
let pendingHostnameFilter = $state(""); let pendingHostnameFilter = $state("");
@ -147,7 +147,14 @@
</Col> </Col>
<!-- Range Col--> <!-- Range Col-->
<Col> <Col>
<TimeSelection bind:from bind:to /> <TimeSelection
presetFrom={from}
presetTo={to}
applyTime={(newFrom, newTo) => {
from = newFrom;
to = newTo;
}}
/>
</Col> </Col>
<!-- Overview Metric Col--> <!-- Overview Metric Col-->
{#if displayNodeOverview} {#if displayNodeOverview}

View File

@ -62,6 +62,7 @@
let isMetricsSelectionOpen = $state(false); let isMetricsSelectionOpen = $state(false);
let sorting = $state({ field: "startTime", type: "col", order: "DESC" }); let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null); let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
let selectedHistogramsBuffer = $state({ all: (ccconfig['user_view_histogramMetrics'] || []) })
let metrics = $state(filterPresets.cluster 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
@ -85,10 +86,7 @@
// let matchedCompareJobs = $state(0); // let matchedCompareJobs = $state(0);
/* Derived Vars */ /* Derived Vars */
let selectedHistograms = $derived(selectedCluster let selectedHistograms = $derived(selectedCluster ? selectedHistogramsBuffer[selectedCluster] : selectedHistogramsBuffer['all']);
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig['user_view_histogramMetrics'] || []);
let stats = $derived( let stats = $derived(
queryStore({ queryStore({
client: client, client: client,
@ -125,8 +123,21 @@
}) })
); );
/* Effect */
$effect(() => {
if (!selectedHistogramsBuffer[selectedCluster]) {
selectedHistogramsBuffer[selectedCluster] = ccconfig[`user_view_histogramMetrics:${selectedCluster}`];
};
});
/* On Mount */ /* On Mount */
onMount(() => filterComponent.updateFilters()); onMount(() => {
filterComponent.updateFilters();
// Why? -> `$derived(ccconfig[$cluster])` only loads array from last Backend-Query if $cluster changed reactively (without reload)
if (filterPresets?.cluster) {
selectedHistogramsBuffer[filterPresets.cluster] = ccconfig[`user_view_histogramMetrics:${filterPresets.cluster}`];
};
});
</script> </script>
<!-- ROW1: Status--> <!-- ROW1: Status-->
@ -363,7 +374,13 @@
</Col> </Col>
</Row> </Row>
<Sorting bind:sorting bind:isOpen={isSortingOpen} /> <Sorting
bind:isOpen={isSortingOpen}
presetSorting={sorting}
applySorting={(newSort) =>
sorting = {...newSort}
}
/>
<MetricSelection <MetricSelection
bind:isOpen={isMetricsSelectionOpen} bind:isOpen={isMetricsSelectionOpen}
@ -378,7 +395,10 @@
/> />
<HistogramSelection <HistogramSelection
bind:cluster={selectedCluster} cluster={selectedCluster}
bind:selectedHistograms
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms}
applyChange={(newSelection) => {
selectedHistogramsBuffer[selectedCluster || 'all'] = [...newSelection];
}}
/> />

View File

@ -20,16 +20,24 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let cluster; /* Svelte 5 Props */
export let selectedHistograms; let {
export let isOpen; cluster,
isOpen = $bindable(),
presetSelectedHistograms,
applyChange
} = $props();
/* Const Init */
const client = getContextClient(); const client = getContextClient();
const initialized = getContext("initialized");
function loadHistoMetrics(isInitialized, thisCluster) { /* Derived */
if (!isInitialized) return []; let selectedHistograms = $derived(presetSelectedHistograms); // Non-Const Derived: Is settable
const availableMetrics = $derived(loadHistoMetrics(cluster));
/* Functions */
function loadHistoMetrics(thisCluster) {
// isInit Check Removed: Parent Component has finished Init-Query: Globalmetrics available here.
if (!thisCluster) { if (!thisCluster) {
return getContext("globalMetrics") return getContext("globalMetrics")
.filter((gm) => gm?.footprint) .filter((gm) => gm?.footprint)
@ -42,18 +50,6 @@
} }
} }
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
function updateConfiguration(data) { function updateConfiguration(data) {
updateConfigurationMutation({ updateConfigurationMutation({
name: data.name, name: data.name,
@ -67,6 +63,7 @@
function closeAndApply() { function closeAndApply() {
isOpen = !isOpen; isOpen = !isOpen;
applyChange(selectedHistograms)
updateConfiguration({ updateConfiguration({
name: cluster name: cluster
? `user_view_histogramMetrics:${cluster}` ? `user_view_histogramMetrics:${cluster}`
@ -75,8 +72,18 @@
}); });
} }
$: availableMetrics = loadHistoMetrics($initialized, cluster); /* Mutation */
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>

View File

@ -7,7 +7,7 @@
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext, onMount } from "svelte";
import { import {
Icon, Icon,
Button, Button,
@ -18,44 +18,73 @@
ModalHeader, ModalHeader,
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { getSortItems } from "../utils.js";
export let isOpen = false; /* Svelte 5 Props */
export let sorting = { field: "startTime", type: "col", order: "DESC" }; let {
isOpen = $bindable(false),
let sortableColumns = []; presetSorting = { field: "startTime", type: "col", order: "DESC" },
let activeColumnIdx; applySorting
} = $props();
/* Const Init */
const initialized = getContext("initialized"); const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
const fixedSortables = $state([
{ field: "startTime", type: "col", text: "Start Time (Default)", order: "DESC" },
{ field: "duration", type: "col", text: "Duration", order: "DESC" },
{ field: "numNodes", type: "col", text: "Number of Nodes", order: "DESC" },
{ field: "numHwthreads", type: "col", text: "Number of HWThreads", order: "DESC" },
{ field: "numAcc", type: "col", text: "Number of Accelerators", order: "DESC" },
{ field: "energy", type: "col", text: "Total Energy", order: "DESC" },
]);
function loadSortables(isInitialized) { /* State Init */
if (!isInitialized) return; let sorting = $state({...presetSorting})
sortableColumns = [ let activeColumnIdx = $state(0);
{ field: "startTime", type: "col", text: "Start Time", order: "DESC" }, let metricSortables = $state([]);
{ field: "duration", type: "col", text: "Duration", order: "DESC" },
{ field: "numNodes", type: "col", text: "Number of Nodes", order: "DESC" },
{ field: "numHwthreads", type: "col", text: "Number of HWThreads", order: "DESC" },
{ field: "numAcc", type: "col", text: "Number of Accelerators", order: "DESC" },
{ field: "energy", type: "col", text: "Total Energy", order: "DESC" },
...getSortItems()
]
}
function loadActiveIndex(isInitialized) { /* Derived */
if (!isInitialized) return; let sortableColumns = $derived([...fixedSortables, ...metricSortables]);
/* Effect */
$effect(() => {
if ($initialized) {
loadMetricSortables();
};
});
/* Functions */
function loadMetricSortables() {
metricSortables = globalMetrics.map((gm) => {
if (gm?.footprint) {
return {
field: gm.name + '_' + gm.footprint,
type: 'foot',
text: gm.name + ' (' + gm.footprint + ')',
order: 'DESC'
}
}
return null
}).filter((r) => r != null)
};
function loadActiveIndex() {
activeColumnIdx = sortableColumns.findIndex( activeColumnIdx = sortableColumns.findIndex(
(col) => col.field == sorting.field, (col) => col.field == sorting.field,
); );
sortableColumns[activeColumnIdx].order = sorting.order; sortableColumns[activeColumnIdx].order = sorting.order;
} }
$: loadSortables($initialized); function resetSorting(sort) {
$: loadActiveIndex($initialized) sorting = {...sort};
loadActiveIndex();
};
</script> </script>
<Modal <Modal
{isOpen} {isOpen}
toggle={() => { toggle={() => {
resetSorting(presetSorting);
isOpen = !isOpen; isOpen = !isOpen;
}} }}
> >
@ -66,7 +95,7 @@
<ListGroupItem> <ListGroupItem>
<button <button
class="sort" class="sort"
on:click={() => { onclick={() => {
if (activeColumnIdx == i) { if (activeColumnIdx == i) {
col.order = col.order == "DESC" ? "ASC" : "DESC"; col.order = col.order == "DESC" ? "ASC" : "DESC";
} else { } else {
@ -96,11 +125,27 @@
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="warning"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
}}>Close</Button resetSorting({ field: "startTime", type: "col", order: "DESC" });
applySorting(sorting);
}}>Reset</Button
> >
<Button
color="primary"
onclick={() => {
applySorting(sorting);
isOpen = false;
}}>Close & Apply</Button
>
<Button
color="secondary"
onclick={() => {
resetSorting(presetSorting);
isOpen = false
}}>Cancel
</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -12,7 +12,6 @@
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
Icon, Icon,
Input, Input,
@ -20,78 +19,96 @@
InputGroupText, InputGroupText,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let from; /* Svelte 5 Props */
export let to; let {
export let customEnabled = true; presetFrom,
export let options = { presetTo,
"Last quarter hour": 15 * 60, customEnabled = true,
"Last half hour": 30 * 60, options = {
"Last hour": 60 * 60, "Last quarter hour": 15 * 60,
"Last 2hrs": 2 * 60 * 60, "Last half hour": 30 * 60,
"Last 4hrs": 4 * 60 * 60, "Last hour": 60 * 60,
"Last 12hrs": 12 * 60 * 60, "Last 2hrs": 2 * 60 * 60,
"Last 24hrs": 24 * 60 * 60, "Last 4hrs": 4 * 60 * 60,
"Last 12hrs": 12 * 60 * 60,
"Last 24hrs": 24 * 60 * 60,
},
applyTime
} = $props();
/* Const Init */
const defaultTo = new Date(Date.now());
const defaultFrom = new Date(defaultTo.setHours(defaultTo.getHours() - 4));
/* State Init */
let timeType = $state("range");
let pendingCustomFrom = $state(null);
let pendingCustomTo = $state(null);
/* Derived */
let timeRange = $derived.by(() => {
if (presetTo && presetFrom) {
return ((presetTo.getTime() - presetFrom.getTime()) / 1000)
} else {
return ((defaultTo.getTime() - defaultFrom.getTime()) / 1000)
}
});
let unknownRange = $derived(!Object.values(options).includes(timeRange));
/* Functions */
function updateTimeRange() {
let now = Date.now();
let t = timeRange * 1000;
applyTime(new Date(now - t), new Date(now));
}; };
$: pendingFrom = from; function updateTimeCustom() {
$: pendingTo = to; if (pendingCustomFrom && pendingCustomTo) {
applyTime(new Date(pendingCustomFrom), new Date(pendingCustomTo));
const dispatch = createEventDispatcher();
let timeRange = // If both times set, return diff, else: display custom select
(to && from) ? ((to.getTime() - from.getTime()) / 1000) : -1;
function updateTimeRange() {
if (timeRange == -1) {
pendingFrom = null;
pendingTo = null;
return;
} }
};
let now = Date.now(),
t = timeRange * 1000;
from = pendingFrom = new Date(now - t);
to = pendingTo = new Date(now);
dispatch("change", { from, to });
}
function updateExplicitTimeRange(type, event) {
let d = new Date(Date.parse(event.target.value));
if (type == "from") pendingFrom = d;
else pendingTo = d;
if (pendingFrom != null && pendingTo != null) {
from = pendingFrom;
to = pendingTo;
dispatch("change", { from, to });
}
}
</script> </script>
<InputGroup class="inline-from"> <InputGroup class="inline-from">
<InputGroupText><Icon name="clock-history" /></InputGroupText> <InputGroupText><Icon name="clock-history" /></InputGroupText>
<InputGroupText>Range</InputGroupText> {#if customEnabled}
<select <Input
class="form-select" type="select"
bind:value={timeRange} style="max-width:fit-content;background-color:#f8f9fa;"
on:change={updateTimeRange} bind:value={timeType}
> >
{#if customEnabled} <option value="range">Range</option>
<option value={-1}>Custom</option> <option value="custom">Custom</option>
{/if} </Input>
{#each Object.entries(options) as [name, seconds]} {:else}
<option value={seconds}>{name}</option> <InputGroupText>Range</InputGroupText>
{/each} {/if}
</select>
{#if timeRange == -1} {#if timeType === "range"}
<Input
type="select"
bind:value={timeRange}
onchange={updateTimeRange}
>
{#if unknownRange}
<option value={timeRange} disabled>Select new range...</option>
{/if}
{#each Object.entries(options) as [name, seconds]}
<option value={seconds}>{name}</option>
{/each}
</Input>
{:else}
<InputGroupText>from</InputGroupText> <InputGroupText>from</InputGroupText>
<Input <Input
type="datetime-local" type="datetime-local"
on:change={(event) => updateExplicitTimeRange("from", event)} bind:value={pendingCustomFrom}
onchange={updateTimeCustom}
></Input> ></Input>
<InputGroupText>to</InputGroupText> <InputGroupText>to</InputGroupText>
<Input <Input
type="datetime-local" type="datetime-local"
on:change={(event) => updateExplicitTimeRange("to", event)} bind:value={pendingCustomTo}
onchange={updateTimeCustom}
></Input> ></Input>
{/if} {/if}
</InputGroup> </InputGroup>

View File

@ -388,24 +388,6 @@ export function findJobFootprintThresholds(job, stat, metricConfig) {
} }
} }
export function getSortItems() {
//console.time('sort')
const globalMetrics = getContext("globalMetrics")
const result = globalMetrics.map((gm) => {
if (gm?.footprint) {
return {
field: gm.name + '_' + gm.footprint,
type: 'foot',
text: gm.name + ' (' + gm.footprint + ')',
order: 'DESC'
}
}
return null
}).filter((r) => r != null)
//console.timeEnd('sort')
return [...result];
};
function getMetricConfigDeep(metric, cluster, subCluster) { function getMetricConfigDeep(metric, cluster, subCluster) {
const clusters = getContext("clusters"); const clusters = getContext("clusters");
if (cluster != null) { if (cluster != null) {