Migrate and fix filter component and subcomponents

This commit is contained in:
Christoph Kluge 2025-06-02 13:00:47 +02:00
parent ffd596e2c7
commit 0b529a5c3c
18 changed files with 800 additions and 816 deletions

View File

@ -24,25 +24,21 @@
import NavbarLinks from "./header/NavbarLinks.svelte"; import NavbarLinks from "./header/NavbarLinks.svelte";
import NavbarTools from "./header/NavbarTools.svelte"; import NavbarTools from "./header/NavbarTools.svelte";
/* Svelte 5 Props */
let { username, authlevel, clusters, subClusters, roles } = $props(); let { username, authlevel, clusters, subClusters, roles } = $props();
let isOpen = $state(false); /* Const Init */
let screenSize = $state(0);
let showMax = $derived(screenSize >= 1500);
let showMid = $derived(screenSize < 1500 && screenSize >= 1300);
let showSml = $derived(screenSize < 1300 && screenSize >= 768);
let showBrg = $derived(screenSize < 768);
const jobsTitle = new Map(); const jobsTitle = new Map();
jobsTitle.set(2, "Job Search"); jobsTitle.set(2, "Job Search");
jobsTitle.set(3, "Managed Jobs"); jobsTitle.set(3, "Managed Jobs");
jobsTitle.set(4, "Jobs"); jobsTitle.set(4, "Jobs");
jobsTitle.set(5, "Jobs"); jobsTitle.set(5, "Jobs");
const usersTitle = new Map(); const usersTitle = new Map();
usersTitle.set(3, "Managed Users"); usersTitle.set(3, "Managed Users");
usersTitle.set(4, "Users"); usersTitle.set(4, "Users");
usersTitle.set(5, "Users"); usersTitle.set(5, "Users");
const projectsTitle = new Map(); const projectsTitle = new Map();
projectsTitle.set(3, "Managed Projects"); projectsTitle.set(3, "Managed Projects");
projectsTitle.set(4, "Projects"); projectsTitle.set(4, "Projects");
@ -122,6 +118,16 @@
menu: "Info", menu: "Info",
}, },
]; ];
/* State Init */
let isOpen = $state(false);
let screenSize = $state(0);
/* Derived Vars */
let showMax = $derived(screenSize >= 1500);
let showMid = $derived(screenSize < 1500 && screenSize >= 1300);
let showSml = $derived(screenSize < 1300 && screenSize >= 768);
let showBrg = $derived(screenSize < 768);
</script> </script>
<svelte:window bind:innerWidth={screenSize} /> <svelte:window bind:innerWidth={screenSize} />

View File

@ -27,13 +27,15 @@
import Sorting from "./generic/select/SortSelection.svelte"; import Sorting from "./generic/select/SortSelection.svelte";
import MetricSelection from "./generic/select/MetricSelection.svelte"; import MetricSelection from "./generic/select/MetricSelection.svelte";
const { query: initq } = init(); /* Svelte 5 Props */
const ccconfig = getContext("cc-config");
// Svelte 5 Props
let { filterPresets, authlevel, roles } = $props(); let { filterPresets, authlevel, roles } = $props();
// Svelte 5 Reactive Vars /* Const Init */
const { query: initq } = init();
const ccconfig = getContext("cc-config");
const presetProject = filterPresets?.project ? filterPresets.project : ""
/* State Init */
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 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 = $state([]); let selectedJobs = $state([]);
let filterBuffer = $state([]); let filterBuffer = $state([]);
@ -56,20 +58,14 @@
: !!ccconfig.plot_list_showFootprint : !!ccconfig.plot_list_showFootprint
); );
// Classic Inits /* Functions */
let presetProject = filterPresets?.project ? filterPresets.project : ""
// The filterPresets are handled by the Filters component,
// 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.
onMount(() => filterComponent.updateFilters());
function resetJobSelection() { function resetJobSelection() {
if (filterComponent && selectedJobs.length === 0) { if (filterComponent && selectedJobs.length === 0) {
filterComponent.updateFilters({ dbId: [] }); filterComponent.updateFilters({ dbId: [] });
}; };
}; };
/* Reactive Effects */
$effect(() => { $effect(() => {
// Reactive : Trigger Effect // Reactive : Trigger Effect
selectedJobs.length selectedJobs.length
@ -79,6 +75,11 @@
}); });
}); });
/* On Mount */
// The filterPresets are handled by the Filters component,
// 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.
onMount(() => filterComponent.updateFilters());
</script> </script>
<!-- ROW1: Status--> <!-- ROW1: Status-->

View File

@ -25,18 +25,18 @@
ButtonDropdown, ButtonDropdown,
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import Tag from "./helper/Tag.svelte";
import Info from "./filters/InfoBox.svelte"; import Info from "./filters/InfoBox.svelte";
import Cluster from "./filters/Cluster.svelte"; import Cluster from "./filters/Cluster.svelte";
import JobStates, { allJobStates } from "./filters/JobStates.svelte"; import JobStates, { allJobStates } from "./filters/JobStates.svelte";
import StartTime from "./filters/StartTime.svelte"; import StartTime, { startTimeSelectOptions } from "./filters/StartTime.svelte";
import Tags from "./filters/Tags.svelte";
import Duration from "./filters/Duration.svelte"; import Duration from "./filters/Duration.svelte";
import Energy from "./filters/Energy.svelte"; import Tags from "./filters/Tags.svelte";
import Tag from "./helper/Tag.svelte";
import Resources from "./filters/Resources.svelte"; import Resources from "./filters/Resources.svelte";
import Energy from "./filters/Energy.svelte";
import Statistics from "./filters/Stats.svelte"; import Statistics from "./filters/Stats.svelte";
// Svelte 5 Props /* Svelte 5 Props */
let { let {
menuText = null, menuText = null,
filterPresets = {}, filterPresets = {},
@ -47,59 +47,55 @@
applyFilters applyFilters
} = $props(); } = $props();
const startTimeSelectOptions = [ /* Const Init */
{ range: "", rangeLabel: "No Selection"},
{ range: "last6h", rangeLabel: "Last 6hrs"},
{ range: "last24h", rangeLabel: "Last 24hrs"},
{ range: "last7d", rangeLabel: "Last 7 days"},
{ range: "last30d", rangeLabel: "Last 30 days"}
];
const nodeMatchLabels = { const nodeMatchLabels = {
eq: "", eq: "",
contains: " Contains", contains: " Contains",
} }
const filterReset = { const filterReset = {
projectMatch: "contains", // Direct Filters
userMatch: "contains", dbId: [],
jobId: "",
jobIdMatch: "eq", jobIdMatch: "eq",
nodeMatch: "eq", arrayJobId: null,
jobName: "",
// View Filters
project: "",
projectMatch: "contains",
user: "",
userMatch: "contains",
// Filter Modals
cluster: null, cluster: null,
partition: null, partition: null,
states: allJobStates, states: allJobStates,
startTime: { from: null, to: null, range: ""}, startTime: { from: null, to: null, range: ""},
tags: [],
duration: { duration: {
lessThan: null, lessThan: null,
moreThan: null, moreThan: null,
from: null, from: null,
to: null, to: null,
}, },
dbId: [], tags: [],
jobId: "",
arrayJobId: null,
user: "",
project: "",
jobName: "",
node: null,
energy: { from: null, to: null },
numNodes: { from: null, to: null }, numNodes: { from: null, to: null },
numHWThreads: { from: null, to: null }, numHWThreads: { from: null, to: null },
numAccelerators: { from: null, to: null }, numAccelerators: { from: null, to: null },
node: null,
nodeMatch: "eq",
energy: { from: null, to: null },
stats: [], stats: [],
}; };
// Svelte 5 Reactive Vars /* State Init */
let filters = $state({ let filters = $state({
projectMatch: filterPresets.projectMatch || "contains", dbId: filterPresets.dbId || [],
userMatch: filterPresets.userMatch || "contains", jobId: filterPresets.jobId || "",
jobIdMatch: filterPresets.jobIdMatch || "eq", jobIdMatch: filterPresets.jobIdMatch || "eq",
nodeMatch: filterPresets.nodeMatch || "eq", arrayJobId: filterPresets.arrayJobId || null,
jobName: filterPresets.jobName || "",
project: filterPresets.project || "",
projectMatch: filterPresets.projectMatch || "contains",
user: filterPresets.user || "",
userMatch: filterPresets.userMatch || "contains",
cluster: filterPresets.cluster || null, cluster: filterPresets.cluster || null,
partition: filterPresets.partition || null, partition: filterPresets.partition || null,
states: states:
@ -107,41 +103,33 @@
? [filterPresets.state].flat() ? [filterPresets.state].flat()
: allJobStates, : allJobStates,
startTime: filterPresets.startTime || { from: null, to: null, range: ""}, startTime: filterPresets.startTime || { from: null, to: null, range: ""},
tags: filterPresets.tags || [],
duration: filterPresets.duration || { duration: filterPresets.duration || {
lessThan: null, lessThan: null,
moreThan: null, moreThan: null,
from: null, from: null,
to: null, to: null,
}, },
dbId: filterPresets.dbId || [], tags: filterPresets.tags || [],
jobId: filterPresets.jobId || "",
arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || "",
project: filterPresets.project || "",
jobName: filterPresets.jobName || "",
node: filterPresets.node || null,
energy: filterPresets.energy || { from: null, to: null },
numNodes: filterPresets.numNodes || { from: null, to: null }, numNodes: filterPresets.numNodes || { from: null, to: null },
numHWThreads: filterPresets.numHWThreads || { from: null, to: null }, numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
numAccelerators: filterPresets.numAccelerators || { from: null, to: null }, numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
node: filterPresets.node || null,
nodeMatch: filterPresets.nodeMatch || "eq",
energy: filterPresets.energy || { from: null, to: null },
stats: filterPresets.stats || [], stats: filterPresets.stats || [],
}); });
/* Opened States */
let isClusterOpen = $state(false) let isClusterOpen = $state(false)
let isJobStatesOpen = $state(false) let isJobStatesOpen = $state(false)
let isStartTimeOpen = $state(false) let isStartTimeOpen = $state(false)
let isTagsOpen = $state(false)
let isDurationOpen = $state(false) let isDurationOpen = $state(false)
let isEnergyOpen = $state(false) let isTagsOpen = $state(false)
let isResourcesOpen = $state(false) let isResourcesOpen = $state(false)
let isEnergyOpen = $state(false)
let isStatsOpen = $state(false) let isStatsOpen = $state(false)
let isNodesModified = $state(false)
let isHwthreadsModified = $state(false)
let isAccsModified = $state(false)
/* Functions */
// 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'
export function updateFilters(additionalFilters = null, force = false) { export function updateFilters(additionalFilters = null, force = false) {
@ -155,10 +143,20 @@
} }
// Construct New Filter // Construct New Filter
let items = []; let items = [];
if (filters.dbId.length != 0)
items.push({ dbId: filters.dbId });
if (filters.jobId)
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
if (filters.arrayJobId != null)
items.push({ arrayJobId: filters.arrayJobId });
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
if (filters.project)
items.push({ project: { [filters.projectMatch]: filters.project } });
if (filters.user)
items.push({ user: { [filters.userMatch]: filters.user } });
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } }); if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
if (filters.partition) items.push({ partition: { eq: filters.partition } }); if (filters.partition) items.push({ partition: { eq: filters.partition } });
if (filters.states.length != allJobStates.length) if (filters.states.length != allJobStates?.length)
items.push({ state: filters.states }); items.push({ state: filters.states });
if (filters.startTime.from || filters.startTime.to) if (filters.startTime.from || filters.startTime.to)
items.push({ items.push({
@ -168,7 +166,6 @@
items.push({ items.push({
startTime: { range: filters.startTime.range }, startTime: { range: filters.startTime.range },
}); });
if (filters.tags.length != 0) items.push({ tags: filters.tags });
if (filters.duration.from || filters.duration.to) if (filters.duration.from || filters.duration.to)
items.push({ items.push({
duration: { from: filters.duration.from, to: filters.duration.to }, duration: { from: filters.duration.from, to: filters.duration.to },
@ -177,21 +174,11 @@
items.push({ duration: { from: 0, to: filters.duration.lessThan } }); items.push({ duration: { from: 0, to: filters.duration.lessThan } });
if (filters.duration.moreThan) if (filters.duration.moreThan)
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes
if (filters.energy.from || filters.energy.to) if (filters.tags.length != 0) items.push({ tags: filters.tags });
items.push({
energy: { from: filters.energy.from, to: filters.energy.to },
});
if (filters.dbId.length != 0)
items.push({ dbId: filters.dbId });
if (filters.jobId)
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
if (filters.arrayJobId != null)
items.push({ arrayJobId: filters.arrayJobId });
if (filters.numNodes.from != null || filters.numNodes.to != null) { if (filters.numNodes.from != null || filters.numNodes.to != null) {
items.push({ items.push({
numNodes: { from: filters.numNodes.from, to: filters.numNodes.to }, numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
}); });
isNodesModified = true;
} }
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) { if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) {
items.push({ items.push({
@ -200,7 +187,6 @@
to: filters.numHWThreads.to, to: filters.numHWThreads.to,
}, },
}); });
isHwthreadsModified = true;
} }
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) { if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) {
items.push({ items.push({
@ -209,13 +195,12 @@
to: filters.numAccelerators.to, to: filters.numAccelerators.to,
}, },
}); });
isAccsModified = true;
} }
if (filters.user) if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
items.push({ user: { [filters.userMatch]: filters.user } }); if (filters.energy.from || filters.energy.to)
if (filters.project) items.push({
items.push({ project: { [filters.projectMatch]: filters.project } }); energy: { from: filters.energy.from, to: filters.energy.to },
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } }); });
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 }} }) });
@ -228,20 +213,7 @@
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000); const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
let opts = []; let opts = [];
if (filters.cluster) opts.push(`cluster=${filters.cluster}`); // Direct Filters
if (filters.node) opts.push(`node=${filters.node}`);
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
opts.push(`nodeMatch=${filters.nodeMatch}`);
if (filters.partition) opts.push(`partition=${filters.partition}`);
if (filters.states.length != allJobStates.length)
for (let state of filters.states) opts.push(`state=${state}`);
if (filters.startTime.from && filters.startTime.to)
opts.push(
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
);
if (filters.startTime.range) {
opts.push(`startTime=${filters.startTime.range}`)
}
if (filters.dbId.length != 0) { if (filters.dbId.length != 0) {
for (let dbi of filters.dbId) { for (let dbi of filters.dbId) {
opts.push(`dbId=${dbi}`); opts.push(`dbId=${dbi}`);
@ -256,21 +228,12 @@
} }
if (filters.jobIdMatch != "eq") if (filters.jobIdMatch != "eq")
opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case
for (let tag of filters.tags) opts.push(`tag=${tag}`); if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
if (filters.duration.from && filters.duration.to) if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`); // View Filters
if (filters.duration.lessThan) if (filters.project) opts.push(`project=${filters.project}`);
opts.push(`duration=0-${filters.duration.lessThan}`); if (filters.project && filters.projectMatch != "contains") // "contains" is default-case
if (filters.duration.moreThan) opts.push(`projectMatch=${filters.projectMatch}`);
opts.push(`duration=${filters.duration.moreThan}-604800`);
if (filters.energy.from && filters.energy.to)
opts.push(`energy=${filters.energy.from}-${filters.energy.to}`);
if (filters.numNodes.from && filters.numNodes.to)
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
if (filters.numHWThreads.from && filters.numHWThreads.to)
opts.push(`numHWThreads=${filters.numHWThreads.from}-${filters.numHWThreads.to}`);
if (filters.numAccelerators.from && filters.numAccelerators.to)
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`);
if (filters.user.length != 0) if (filters.user.length != 0)
if (filters.userMatch != "in") { if (filters.userMatch != "in") {
opts.push(`user=${filters.user}`); opts.push(`user=${filters.user}`);
@ -279,16 +242,42 @@
} }
if (filters.userMatch != "contains") // "contains" is default-case if (filters.userMatch != "contains") // "contains" is default-case
opts.push(`userMatch=${filters.userMatch}`); opts.push(`userMatch=${filters.userMatch}`);
if (filters.project) opts.push(`project=${filters.project}`); // Filter Modals
if (filters.project && filters.projectMatch != "contains") // "contains" is default-case if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
opts.push(`projectMatch=${filters.projectMatch}`); if (filters.partition) opts.push(`partition=${filters.partition}`);
if (filters.jobName) opts.push(`jobName=${filters.jobName}`); if (filters.states.length != allJobStates?.length)
if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`); for (let state of filters.states) opts.push(`state=${state}`);
if (filters.startTime.from && filters.startTime.to)
opts.push(
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
);
if (filters.startTime.range) {
opts.push(`startTime=${filters.startTime.range}`)
}
if (filters.duration.from && filters.duration.to)
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
if (filters.duration.lessThan)
opts.push(`duration=0-${filters.duration.lessThan}`);
if (filters.duration.moreThan)
opts.push(`duration=${filters.duration.moreThan}-604800`);
if (filters.tags.length != 0)
for (let tag of filters.tags) opts.push(`tag=${tag}`);
if (filters.numNodes.from && filters.numNodes.to)
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
if (filters.numHWThreads.from && filters.numHWThreads.to)
opts.push(`numHWThreads=${filters.numHWThreads.from}-${filters.numHWThreads.to}`);
if (filters.numAccelerators.from && filters.numAccelerators.to)
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`);
if (filters.node) opts.push(`node=${filters.node}`);
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
opts.push(`nodeMatch=${filters.nodeMatch}`);
if (filters.energy.from && filters.energy.to)
opts.push(`energy=${filters.energy.from}-${filters.energy.to}`);
if (filters.stats.length != 0) if (filters.stats.length != 0)
for (let stat of filters.stats) { for (let stat of filters.stats) {
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`); opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
} }
// Build && Return
if (opts.length == 0 && window.location.search.length <= 1) return; if (opts.length == 0 && window.location.search.length <= 1) return;
let newurl = `${window.location.pathname}?${opts.join("&")}`; let newurl = `${window.location.pathname}?${opts.join("&")}`;
window.history.replaceState(null, "", newurl); window.history.replaceState(null, "", newurl);
@ -309,36 +298,36 @@
<DropdownItem disabled>{menuText}</DropdownItem> <DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider /> <DropdownItem divider />
{/if} {/if}
<DropdownItem on:click={() => (isClusterOpen = true)}> <DropdownItem onclick={() => (isClusterOpen = true)}>
<Icon name="cpu" /> Cluster/Partition <Icon name="cpu" /> Cluster/Partition
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}> <DropdownItem onclick={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill" /> Job States <Icon name="gear-fill" /> Job States
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}> <DropdownItem onclick={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range" /> Start Time <Icon name="calendar-range" /> Start Time
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}> <DropdownItem onclick={() => (isDurationOpen = true)}>
<Icon name="stopwatch" /> Duration <Icon name="stopwatch" /> Duration
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}> <DropdownItem onclick={() => (isTagsOpen = true)}>
<Icon name="tags" /> Tags <Icon name="tags" /> Tags
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}> <DropdownItem onclick={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack" /> Resources <Icon name="hdd-stack" /> Resources
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isEnergyOpen = true)}> <DropdownItem onclick={() => (isEnergyOpen = true)}>
<Icon name="lightning-charge-fill" /> Energy <Icon name="lightning-charge-fill" /> Energy
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}> <DropdownItem onclick={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics <Icon name="bar-chart" onclick={() => (isStatsOpen = true)} /> Statistics
</DropdownItem> </DropdownItem>
{#if startTimeQuickSelect} {#if startTimeQuickSelect}
<DropdownItem divider /> <DropdownItem divider />
<DropdownItem disabled>Start Time Quick Selection</DropdownItem> <DropdownItem disabled>Start Time Quick Selection</DropdownItem>
{#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }} {#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
<DropdownItem <DropdownItem
on:click={() => { onclick={() => {
filters.startTime.from = null filters.startTime.from = null
filters.startTime.to = null filters.startTime.to = null
filters.startTime.range = range; filters.startTime.range = range;
@ -364,7 +353,7 @@
{#if showFilter} {#if showFilter}
<!-- SELECTED FILTER PILLS --> <!-- SELECTED FILTER PILLS -->
{#if filters.cluster} {#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}> <Info icon="cpu" onclick={() => (isClusterOpen = true)}>
{filters.cluster} {filters.cluster}
{#if filters.partition} {#if filters.partition}
({filters.partition}) ({filters.partition})
@ -372,14 +361,14 @@
</Info> </Info>
{/if} {/if}
{#if filters.states.length != allJobStates.length} {#if filters.states.length != allJobStates?.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}> <Info icon="gear-fill" onclick={() => (isJobStatesOpen = true)}>
{filters.states.join(", ")} {filters.states.join(", ")}
</Info> </Info>
{/if} {/if}
{#if filters.startTime.from || filters.startTime.to} {#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}> <Info icon="calendar-range" onclick={() => (isStartTimeOpen = true)}>
{new Date(filters.startTime.from).toLocaleString()} - {new Date( {new Date(filters.startTime.from).toLocaleString()} - {new Date(
filters.startTime.to, filters.startTime.to,
).toLocaleString()} ).toLocaleString()}
@ -387,13 +376,13 @@
{/if} {/if}
{#if filters.startTime.range} {#if filters.startTime.range}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}> <Info icon="calendar-range" onclick={() => (isStartTimeOpen = true)}>
{startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel } {startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel }
</Info> </Info>
{/if} {/if}
{#if filters.duration.from || filters.duration.to} {#if filters.duration.from || filters.duration.to}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
{Math.floor(filters.duration.from / 3600)}h:{Math.floor( {Math.floor(filters.duration.from / 3600)}h:{Math.floor(
(filters.duration.from % 3600) / 60, (filters.duration.from % 3600) / 60,
)}m - )}m -
@ -404,7 +393,7 @@
{/if} {/if}
{#if filters.duration.lessThan} {#if filters.duration.lessThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
Duration less than {Math.floor( Duration less than {Math.floor(
filters.duration.lessThan / 3600, filters.duration.lessThan / 3600,
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m )}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
@ -412,7 +401,7 @@
{/if} {/if}
{#if filters.duration.moreThan} {#if filters.duration.moreThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
Duration more than {Math.floor( Duration more than {Math.floor(
filters.duration.moreThan / 3600, filters.duration.moreThan / 3600,
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m )}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
@ -420,47 +409,45 @@
{/if} {/if}
{#if filters.tags.length != 0} {#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}> <Info icon="tags" onclick={() => (isTagsOpen = true)}>
{#each filters.tags as tagId} {#each filters.tags as tagId}
{#key tagId} <Tag id={tagId} clickable={false} />
<Tag id={tagId} clickable={false} />
{/key}
{/each} {/each}
</Info> </Info>
{/if} {/if}
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null} {#if filters.numNodes.from != null || filters.numNodes.to != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> <Info icon="hdd-stack" onclick={() => (isResourcesOpen = true)}>
{#if isNodesModified}
Nodes: {filters.numNodes.from} - {filters.numNodes.to} Nodes: {filters.numNodes.from} - {filters.numNodes.to}
{/if} </Info>
{#if isNodesModified && isHwthreadsModified}, {/if}
{/if}
{#if isHwthreadsModified} {#if filters.numHWThreads.from != null || filters.numHWThreads.to != null}
<Info icon="cpu" onclick={() => (isResourcesOpen = true)}>
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to} HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
{/if} </Info>
{#if (isNodesModified || isHwthreadsModified) && isAccsModified}, {/if}
{/if}
{#if isAccsModified} {#if filters.numAccelerators.from != null || filters.numAccelerators.to != null}
<Info icon="gpu-card" onclick={() => (isResourcesOpen = true)}>
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to} Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
{/if}
</Info> </Info>
{/if} {/if}
{#if filters.node != null} {#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> <Info icon="hdd-stack" onclick={() => (isResourcesOpen = true)}>
Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node} Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node}
</Info> </Info>
{/if} {/if}
{#if filters.energy.from || filters.energy.to} {#if filters.energy.from || filters.energy.to}
<Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}> <Info icon="lightning-charge-fill" onclick={() => (isEnergyOpen = true)}>
Total Energy: {filters.energy.from} - {filters.energy.to} Total Energy: {filters.energy.from} - {filters.energy.to}
</Info> </Info>
{/if} {/if}
{#if filters.stats.length > 0} {#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}> <Info icon="bar-chart" onclick={() => (isStatsOpen = true)}>
{filters.stats {filters.stats
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`) .map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
.join(", ")} .join(", ")}
@ -469,69 +456,62 @@
{/if} {/if}
<Cluster <Cluster
{disableClusterSelection}
bind:isOpen={isClusterOpen} bind:isOpen={isClusterOpen}
bind:cluster={filters.cluster} presetCluster={filters.cluster}
bind:partition={filters.partition} presetPartition={filters.partition}
on:set-filter={() => updateFilters()} {disableClusterSelection}
setFilter={(filter) => updateFilters(filter)}
/> />
<JobStates <JobStates
bind:isOpen={isJobStatesOpen} bind:isOpen={isJobStatesOpen}
bind:states={filters.states} presetStates={filters.states}
on:set-filter={() => updateFilters()} setFilter={(filter) => updateFilters(filter)}
/> />
<StartTime <StartTime
bind:isOpen={isStartTimeOpen} bind:isOpen={isStartTimeOpen}
bind:from={filters.startTime.from} presetStartTime={filters.startTime}
bind:to={filters.startTime.to} setFilter={(filter) => updateFilters(filter)}
bind:range={filters.startTime.range}
{startTimeSelectOptions}
on:set-filter={() => updateFilters()}
/> />
<Duration <Duration
bind:isOpen={isDurationOpen} bind:isOpen={isDurationOpen}
bind:lessThan={filters.duration.lessThan} presetDuration={filters.duration}
bind:moreThan={filters.duration.moreThan} setFilter={(filter) => updateFilters(filter)}
bind:from={filters.duration.from}
bind:to={filters.duration.to}
on:set-filter={() => updateFilters()}
/> />
<Tags <Tags
bind:isOpen={isTagsOpen} bind:isOpen={isTagsOpen}
bind:tags={filters.tags} presetTags={filters.tags}
on:set-filter={() => updateFilters()} setFilter={(filter) => updateFilters(filter)}
/> />
<Resources <Resources
cluster={filters.cluster}
bind:isOpen={isResourcesOpen} bind:isOpen={isResourcesOpen}
bind:numNodes={filters.numNodes} activeCluster={filters.cluster}
bind:numHWThreads={filters.numHWThreads} presetNumNodes={filters.numNodes}
bind:numAccelerators={filters.numAccelerators} presetNumHWThreads={filters.numHWThreads}
bind:namedNode={filters.node} presetNumAccelerators={filters.numAccelerators}
bind:nodeMatch={filters.nodeMatch} presetNamedNode={filters.node}
bind:isNodesModified presetNodeMatch={filters.nodeMatch}
bind:isHwthreadsModified setFilter={(filter) => updateFilters(filter)}
bind:isAccsModified
on:set-filter={() => updateFilters()}
/>
<Statistics
bind:isOpen={isStatsOpen}
bind:stats={filters.stats}
on:set-filter={() => updateFilters()}
/> />
<Energy <Energy
bind:isOpen={isEnergyOpen} bind:isOpen={isEnergyOpen}
bind:energy={filters.energy} presetEnergy={filters.energy}
on:set-filter={() => updateFilters()} setFilter={(filter) => updateFilters(filter)}
/> />
<Statistics
bind:isOpen={isStatsOpen}
presetStats={filters.stats}
setFilter={(filter) => updateFilters(filter)}
/>
<style> <style>
:global(.cc-dropdown-on-hover:hover .dropdown-menu) { :global(.cc-dropdown-on-hover:hover .dropdown-menu) {
display: block; display: block;

View File

@ -13,7 +13,7 @@
--> -->
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { getContext } from "svelte";
import { import {
Button, Button,
ListGroup, ListGroup,
@ -24,18 +24,23 @@
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
const clusters = getContext("clusters"), /* Svelte 5 Props */
initialized = getContext("initialized"), let {
dispatch = createEventDispatcher(); isOpen = $bindable(false),
presetCluster = "",
presetPartition = "",
disableClusterSelection = false,
setFilter
} = $props();
/* State Init */
let pendingCluster = $state(presetCluster);
let pendingPartition = $state(presetPartition);
/* Derived Vars */
const clusters = $derived(getContext("clusters"));
const initialized = $derived(getContext("initialized"));
export let disableClusterSelection = false;
export let isModified = false;
export let isOpen = false;
export let cluster = null;
export let partition = null;
let pendingCluster = cluster,
pendingPartition = partition;
$: isModified = pendingCluster != cluster || pendingPartition != partition;
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@ -45,13 +50,13 @@
<h4>Cluster</h4> <h4>Cluster</h4>
{#if disableClusterSelection} {#if disableClusterSelection}
<Button color="info" class="w-100 mb-2" disabled><b>Info: Cluster Selection Disabled in This View</b></Button> <Button color="info" class="w-100 mb-2" disabled><b>Info: Cluster Selection Disabled in This View</b></Button>
<Button outline color="primary" class="w-100 mb-2" disabled><b>Selected Cluster: {cluster}</b></Button> <Button outline color="primary" class="w-100 mb-2" disabled><b>Selected Cluster: {presetCluster}</b></Button>
{:else} {:else}
<ListGroup> <ListGroup>
<ListGroupItem <ListGroupItem
disabled={disableClusterSelection} disabled={disableClusterSelection}
active={pendingCluster == null} active={pendingCluster == null}
on:click={() => ((pendingCluster = null), (pendingPartition = null))} onclick={() => ((pendingCluster = null), (pendingPartition = null))}
> >
Any Cluster Any Cluster
</ListGroupItem> </ListGroupItem>
@ -59,7 +64,7 @@
<ListGroupItem <ListGroupItem
disabled={disableClusterSelection} disabled={disableClusterSelection}
active={pendingCluster == cluster.name} active={pendingCluster == cluster.name}
on:click={() => ( onclick={() => (
(pendingCluster = cluster.name), (pendingPartition = null) (pendingCluster = cluster.name), (pendingPartition = null)
)} )}
> >
@ -75,14 +80,14 @@
<ListGroup> <ListGroup>
<ListGroupItem <ListGroupItem
active={pendingPartition == null} active={pendingPartition == null}
on:click={() => (pendingPartition = null)} onclick={() => (pendingPartition = null)}
> >
Any Partition Any Partition
</ListGroupItem> </ListGroupItem>
{#each clusters.find((c) => c.name == pendingCluster).partitions as partition} {#each clusters?.find((c) => c.name == pendingCluster)?.partitions as partition}
<ListGroupItem <ListGroupItem
active={pendingPartition == partition} active={pendingPartition == partition}
on:click={() => (pendingPartition = partition)} onclick={() => (pendingPartition = partition)}
> >
{partition} {partition}
</ListGroupItem> </ListGroupItem>
@ -93,22 +98,20 @@
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
cluster = pendingCluster; setFilter({ cluster: pendingCluster, partition: pendingPartition });
partition = pendingPartition;
dispatch("set-filter", { cluster, partition });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
cluster = pendingCluster = null; pendingCluster = null;
partition = pendingPartition = null; pendingPartition = null;
dispatch("set-filter", { cluster, partition }); setFilter({ cluster: pendingCluster, partition: pendingPartition})
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -13,7 +13,6 @@
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
Row, Row,
Col, Col,
@ -24,61 +23,81 @@
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
isOpen = $bindable(false),
presetDuration ={lessThan: null, moreThan: null, from: null, to: null},
setFilter
} = $props();
export let isOpen = false; /* State Init */
export let lessThan = null; let pendingDuration = $state(presetDuration);
export let moreThan = null; let lessState = $state(secsToHoursAndMins(presetDuration?.lessThan));
export let from = null; let moreState = $state(secsToHoursAndMins(presetDuration?.moreThan));
export let to = null; let fromState = $state(secsToHoursAndMins(presetDuration?.from));
let toState = $state(secsToHoursAndMins(presetDuration?.to));
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo; /* Derived Init */
let lessDisabled = false, const lessDisabled = $derived(
moreDisabled = false, moreState.hours !== 0 ||
betweenDisabled = false; moreState.mins !== 0 ||
fromState.hours !== 0 ||
fromState.mins !== 0 ||
toState.hours !== 0 ||
toState.mins !== 0
);
function reset() { const moreDisabled = $derived(
pendingLessThan = lessState.hours !== 0 ||
lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan); lessState.mins !== 0 ||
pendingMoreThan = fromState.hours !== 0 ||
moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan); fromState.mins !== 0 ||
pendingFrom = toState.hours !== 0 ||
from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from); toState.mins !== 0
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to); );
const betweenDisabled = $derived(
moreState.hours !== 0 ||
moreState.mins !== 0 ||
lessState.hours !== 0 ||
lessState.mins !== 0
)
/* Functions */
function resetPending() {
pendingDuration = {
lessThan: null,
moreThan: null,
from: null,
to: null
}
};
function resetStates() {
lessState = { hours: 0, mins: 0 }
moreState = { hours: 0, mins: 0 }
fromState = { hours: 0, mins: 0 }
toState = { hours: 0, mins: 0 }
};
function secsToHoursAndMins(seconds) {
if (seconds) {
const hours = Math.floor(seconds / 3600);
seconds -= hours * 3600;
const mins = Math.floor(seconds / 60);
return { hours, mins };
} else {
return { hours: 0, mins: 0 }
}
} }
reset(); function hoursAndMinsToSecs(hoursAndMins) {
if (hoursAndMins) {
function secsToHoursAndMins(duration) { return hoursAndMins.hours * 3600 + hoursAndMins.mins * 60;
const hours = Math.floor(duration / 3600); } else {
duration -= hours * 3600; return 0
const mins = Math.floor(duration / 60); }
return { hours, mins };
} }
function hoursAndMinsToSecs({ hours, mins }) {
return hours * 3600 + mins * 60;
}
$: lessDisabled =
pendingMoreThan.hours !== 0 ||
pendingMoreThan.mins !== 0 ||
pendingFrom.hours !== 0 ||
pendingFrom.mins !== 0 ||
pendingTo.hours !== 0 ||
pendingTo.mins !== 0;
$: moreDisabled =
pendingLessThan.hours !== 0 ||
pendingLessThan.mins !== 0 ||
pendingFrom.hours !== 0 ||
pendingFrom.mins !== 0 ||
pendingTo.hours !== 0 ||
pendingTo.mins !== 0;
$: betweenDisabled =
pendingMoreThan.hours !== 0 ||
pendingMoreThan.mins !== 0 ||
pendingLessThan.hours !== 0 ||
pendingLessThan.mins !== 0;
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@ -92,7 +111,7 @@
type="number" type="number"
min="0" min="0"
class="form-control" class="form-control"
bind:value={pendingMoreThan.hours} bind:value={moreState.hours}
disabled={moreDisabled} disabled={moreDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@ -107,7 +126,7 @@
min="0" min="0"
max="59" max="59"
class="form-control" class="form-control"
bind:value={pendingMoreThan.mins} bind:value={moreState.mins}
disabled={moreDisabled} disabled={moreDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@ -126,7 +145,7 @@
type="number" type="number"
min="0" min="0"
class="form-control" class="form-control"
bind:value={pendingLessThan.hours} bind:value={lessState.hours}
disabled={lessDisabled} disabled={lessDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@ -141,7 +160,7 @@
min="0" min="0"
max="59" max="59"
class="form-control" class="form-control"
bind:value={pendingLessThan.mins} bind:value={lessState.mins}
disabled={lessDisabled} disabled={lessDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@ -160,7 +179,7 @@
type="number" type="number"
min="0" min="0"
class="form-control" class="form-control"
bind:value={pendingFrom.hours} bind:value={fromState.hours}
disabled={betweenDisabled} disabled={betweenDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@ -175,7 +194,7 @@
min="0" min="0"
max="59" max="59"
class="form-control" class="form-control"
bind:value={pendingFrom.mins} bind:value={fromState.mins}
disabled={betweenDisabled} disabled={betweenDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@ -192,7 +211,7 @@
type="number" type="number"
min="0" min="0"
class="form-control" class="form-control"
bind:value={pendingTo.hours} bind:value={toState.hours}
disabled={betweenDisabled} disabled={betweenDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@ -207,7 +226,7 @@
min="0" min="0"
max="59" max="59"
class="form-control" class="form-control"
bind:value={pendingTo.mins} bind:value={toState.mins}
disabled={betweenDisabled} disabled={betweenDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@ -220,39 +239,32 @@
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
lessThan = hoursAndMinsToSecs(pendingLessThan); pendingDuration.lessThan = hoursAndMinsToSecs(lessState);
moreThan = hoursAndMinsToSecs(pendingMoreThan); pendingDuration.moreThan = hoursAndMinsToSecs(moreState);
from = hoursAndMinsToSecs(pendingFrom); pendingDuration.from = hoursAndMinsToSecs(fromState);
to = hoursAndMinsToSecs(pendingTo); pendingDuration.to = hoursAndMinsToSecs(toState);
dispatch("set-filter", { lessThan, moreThan, from, to }); setFilter({duration: pendingDuration});
}} }}
> >
Close & Apply Close & Apply
</Button> </Button>
<Button <Button
color="warning" color="warning"
on:click={() => { onclick={() => {
lessThan = null; resetStates();
moreThan = null;
from = null;
to = null;
reset();
}}>Reset Values</Button }}>Reset Values</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
lessThan = null; resetStates();
moreThan = null; resetPending();
from = null; setFilter({duration: pendingDuration});
to = null;
reset();
dispatch("set-filter", { lessThan, moreThan, from, to });
}}>Reset Filter</Button }}>Reset Filter</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -10,7 +10,6 @@
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
Button, Button,
Modal, Modal,
@ -20,49 +19,49 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
isOpen = $bindable(false),
presetEnergy= {from: null, to: null},
setFilter,
} = $props();
const energyMaximum = 1000.0; /* State Init */
let energyState = $state(presetEnergy);
export let isOpen = false;
export let energy= {from: null, to: null};
function resetRanges() {
energy.from = null
energy.to = null
}
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Filter based on energy</ModalHeader> <ModalHeader>Filter based on energy</ModalHeader>
<ModalBody> <ModalBody>
<h4>Total Job Energy (kWh)</h4> <div class="mb-3">
<DoubleRangeSlider <div class="mb-0"><b>Total Job Energy (kWh)</b></div>
on:change={({ detail }) => ( <DoubleRangeSlider
(energy.from = detail[0]), (energy.to = detail[1]) changeRange={(detail) => {
)} energyState.from = detail[0];
min={0.0} energyState.to = detail[1];
max={energyMaximum} }}
firstSlider={energy?.from ? energy.from : 0.0} sliderMin={0.0}
secondSlider={energy?.to ? energy.to : energyMaximum} sliderMax={1000.0}
inputFieldFrom={energy?.from ? energy.from : null} fromPreset={energyState?.from? energyState.from : 0.0}
inputFieldTo={energy?.to ? energy.to : null} toPreset={energyState?.to? energyState.to : 1000.0}
/> />
</div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { on:click={() => {
isOpen = false; isOpen = false;
dispatch("set-filter", { energy }); setFilter({ energy: energyState });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { on:click={() => {
isOpen = false; isOpen = false;
resetRanges(); energyState = {from: null, to: null};
dispatch("set-filter", { energy }); setFilter({ energy: energyState });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>

View File

@ -4,16 +4,22 @@
Properties: Properties:
- `icon String`: Sveltestrap icon name - `icon String`: Sveltestrap icon name
- `modified Bool?`: Optional if filter is modified [Default: false] - `modified Bool?`: Optional if filter is modified [Default: false]
- `onclick Fn()`: Opens Modal on click
- `children Fn()?`: Internal prop, Svelte 5 version of <slot/>
--> -->
<script> <script>
import { Button, Icon } from "@sveltestrap/sveltestrap"; import { Button, Icon } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */
export let icon; let { icon, modified, onclick, children } = $props();
export let modified = false;
</script> </script>
<Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} on:click> <Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} {onclick}>
<Icon name={icon} /> <Icon name={icon} />
<slot /> {#if children}
<!-- Note: Ignore '@' Error in IDE -->
{@render children()}
{:else}
<span>No content found</span>
{/if}
</Button> </Button>

View File

@ -13,7 +13,7 @@
- `const allJobStates [String]`: List of all available job states used in cc-backend - `const allJobStates [String]`: List of all available job states used in cc-backend
--> -->
<script context="module"> <script module>
export const allJobStates = [ export const allJobStates = [
"running", "running",
"completed", "completed",
@ -27,7 +27,8 @@
</script> </script>
<script> <script>
import { createEventDispatcher } from "svelte"; /* Note: Ignore VSCode reported 'A component can only have one instance-level <script> element' error */
import { import {
Button, Button,
ListGroup, ListGroup,
@ -38,16 +39,16 @@
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
isOpen = $bindable(false),
presetStates = [...allJobStates],
setFilter
} = $props();
export let isModified = false; /* State Init */
export let isOpen = false; let pendingStates = $state([...presetStates]);
export let states = [...allJobStates];
let pendingStates = [...states];
$: isModified =
states.length != pendingStates.length ||
!states.every((state) => pendingStates.includes(state));
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@ -71,28 +72,25 @@
<Button <Button
color="primary" color="primary"
disabled={pendingStates.length == 0} disabled={pendingStates.length == 0}
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
states = [...pendingStates]; setFilter({ states: [...pendingStates] });
dispatch("set-filter", { states });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="warning" color="warning"
on:click={() => { onclick={() => {
states = [...allJobStates];
pendingStates = []; pendingStates = [];
}}>Deselect All</Button }}>Deselect All</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
states = [...allJobStates];
pendingStates = [...allJobStates]; pendingStates = [...allJobStates];
dispatch("set-filter", { states }); setFilter({ states: [...pendingStates] });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -2,14 +2,11 @@
@component Filter sub-component for selecting job resources @component Filter sub-component for selecting job resources
Properties: Properties:
- `cluster Object?`: The currently selected cluster config [Default: null]
- `isOpen Bool?`: Is this filter component opened [Default: false] - `isOpen Bool?`: Is this filter component opened [Default: false]
- `activeCluster String?`: The currently selected cluster name [Default: null]
- `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}] - `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}]
- `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}] - `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}]
- `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}] - `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}]
- `isNodesModified Bool?`: Is the node filter modified [Default: false]
- `isHwthreadsModified Bool?`: Is the Hwthreads filter modified [Default: false]
- `isAccsModified Bool?`: Is the Accelerator filter modified [Default: false]
- `namedNode String?`: The currently selected single named node (= hostname) [Default: null] - `namedNode String?`: The currently selected single named node (= hostname) [Default: null]
Events: Events:
@ -17,7 +14,7 @@
--> -->
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { getContext } from "svelte";
import { import {
Button, Button,
Modal, Modal,
@ -28,27 +25,19 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
const clusters = getContext("clusters"), /* Svelte 5 Props*/
initialized = getContext("initialized"), let {
dispatch = createEventDispatcher(); isOpen = $bindable(false),
activeCluster = null,
export let cluster = null; presetNumNodes = { from: null, to: null },
export let isOpen = false; presetNumHWThreads = { from: null, to: null },
export let numNodes = { from: null, to: null }; presetNumAccelerators = { from: null, to: null },
export let numHWThreads = { from: null, to: null }; presetNamedNode = null,
export let numAccelerators = { from: null, to: null }; presetNodeMatch = "eq",
export let isNodesModified = false; setFilter
export let isHwthreadsModified = false; } = $props()
export let isAccsModified = false;
export let namedNode = null;
export let nodeMatch = "eq"
let pendingNumNodes = numNodes,
pendingNumHWThreads = numHWThreads,
pendingNumAccelerators = numAccelerators,
pendingNamedNode = namedNode,
pendingNodeMatch = nodeMatch;
/* Const Init */
const nodeMatchLabels = { const nodeMatchLabels = {
eq: "Equal To", eq: "Equal To",
contains: "Contains", contains: "Contains",
@ -85,75 +74,133 @@
0, 0,
); );
let minNumNodes = 1, /* State Init*/
maxNumNodes = 0, // Counts
minNumHWThreads = 1, let minNumNodes = $state(1);
maxNumHWThreads = 0, let maxNumNodes = $state(0);
minNumAccelerators = 0, let maxNumHWThreads = $state(0);
maxNumAccelerators = 0; let maxNumAccelerators = $state(0);
$: { // Pending
let pendingNumNodes = $state(presetNumNodes);
let pendingNumHWThreads = $state(presetNumHWThreads);
let pendingNumAccelerators = $state(presetNumAccelerators);
let pendingNamedNode = $state(presetNamedNode);
let pendingNodeMatch = $state(presetNodeMatch);
// Changable States
let nodesState = $state(presetNumNodes);
let threadState = $state(presetNumHWThreads);
let accState = $state(presetNumAccelerators);
/* Derived States */
const clusters = $derived(getContext("clusters"));
const initialized = $derived(getContext("initialized"));
// Is Selection Active
const nodesActive = $derived(!(JSON.stringify(nodesState) === JSON.stringify({ from: 1, to: maxNumNodes })));
const threadActive = $derived(!(JSON.stringify(threadState) === JSON.stringify({ from: 1, to: maxNumHWThreads })));
const accActive = $derived(!(JSON.stringify(accState) === JSON.stringify({ from: 0, to: maxNumAccelerators })));
// Block Apply if null
const disableApply = $derived(
nodesState.from === null || nodesState.to === null ||
threadState.from === null || threadState.to === null ||
accState.from === null || accState.to === null
);
/* Reactive Effects | Svelte 5 onMount */
$effect(() => {
if ($initialized) { if ($initialized) {
if (cluster != null) { // 'hClusters' defined in templates/base.tmpl
const { subClusters } = clusters.find((c) => c.name == cluster); if (activeCluster != null) {
const { filterRanges } = header.clusters.find((c) => c.name == cluster); const { filterRanges } = hClusters.find((c) => c.name == activeCluster);
minNumNodes = filterRanges.numNodes.from; minNumNodes = filterRanges.numNodes.from;
maxNumNodes = filterRanges.numNodes.to; maxNumNodes = filterRanges.numNodes.to;
} else if (clusters.length > 0) {
for (let hc of hClusters) {
const { filterRanges } = hc;
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
};
};
};
});
$effect(() => {
if ($initialized) {
// 'hClusters' defined in templates/base.tmpl
if (activeCluster != null) {
const { subClusters } = clusters.find((c) => c.name == activeCluster);
maxNumAccelerators = findMaxNumAccels([{ subClusters }]); maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]); maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]);
} else if (clusters.length > 0) { } else if (clusters.length > 0) {
const { filterRanges } = header.clusters[0];
minNumNodes = filterRanges.numNodes.from;
maxNumNodes = filterRanges.numNodes.to;
maxNumAccelerators = findMaxNumAccels(clusters); maxNumAccelerators = findMaxNumAccels(clusters);
maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters); maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters);
for (let cluster of header.clusters) {
const { filterRanges } = cluster;
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
}
} }
} }
} });
$: { $effect(() => {
if ( if (
isOpen &&
$initialized && $initialized &&
pendingNumNodes.from == null && pendingNumNodes.from == null &&
pendingNumNodes.to == null pendingNumNodes.to == null
) { ) {
pendingNumNodes = { from: 0, to: maxNumNodes }; nodesState = { from: 1, to: maxNumNodes };
} }
} });
$: { $effect(() => {
if ( if (
isOpen &&
$initialized && $initialized &&
((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) || pendingNumHWThreads.from == null &&
isHwthreadsModified == false) pendingNumHWThreads.to == null
) { ) {
pendingNumHWThreads = { from: 0, to: maxNumHWThreads }; threadState = { from: 1, to: maxNumHWThreads };
} }
} });
$: if (maxNumAccelerators != null && maxNumAccelerators > 1) { $effect(() => {
if ( if (
isOpen &&
$initialized && $initialized &&
pendingNumAccelerators.from == null && pendingNumAccelerators.from == null &&
pendingNumAccelerators.to == null pendingNumAccelerators.to == null
) { ) {
pendingNumAccelerators = { from: 0, to: maxNumAccelerators }; accState = { from: 0, to: maxNumAccelerators };
} }
} });
/* Functions */
function setResources() {
if (nodesActive) {
pendingNumNodes = {...nodesState};
} else {
pendingNumNodes = { from: null, to: null };
};
if (threadActive) {
pendingNumHWThreads = {...threadState};
} else {
pendingNumHWThreads = { from: null, to: null };
};
if (accActive) {
pendingNumAccelerators = {...accState};
} else {
pendingNumAccelerators = { from: null, to: null };
};
};
function resetResources() {
pendingNumNodes = { from: null, to: null };
pendingNumHWThreads = { from: null, to: null };
pendingNumAccelerators = { from: null, to: null };
pendingNamedNode = null;
pendingNodeMatch = "eq";
};
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select number of utilized Resources</ModalHeader> <ModalHeader>Select number of utilized Resources</ModalHeader>
<ModalBody> <ModalBody>
<h6>Named Node</h6> <div><b>Named Node</b></div>
<div class="d-flex"> <div class="d-flex mb-3">
<Input type="text" class="w-75" bind:value={pendingNamedNode} /> <Input type="text" class="w-75" bind:value={pendingNamedNode} />
<div class="mx-1"></div> <div class="mx-1"></div>
<Input type="select" class="w-25" bind:value={pendingNodeMatch}> <Input type="select" class="w-25" bind:value={pendingNodeMatch}>
@ -164,82 +211,63 @@
{/each} {/each}
</Input> </Input>
</div> </div>
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
<DoubleRangeSlider <div class="mb-3">
on:change={({ detail }) => { <div class="mb-0"><b>Number of Nodes</b></div>
pendingNumNodes = { from: detail[0], to: detail[1] };
isNodesModified = true;
}}
min={minNumNodes}
max={maxNumNodes}
firstSlider={pendingNumNodes.from}
secondSlider={pendingNumNodes.to}
inputFieldFrom={pendingNumNodes.from}
inputFieldTo={pendingNumNodes.to}
/>
<h6 style="margin-top: 1rem;">
Number of HWThreads (Use for Single-Node Jobs)
</h6>
<DoubleRangeSlider
on:change={({ detail }) => {
pendingNumHWThreads = { from: detail[0], to: detail[1] };
isHwthreadsModified = true;
}}
min={minNumHWThreads}
max={maxNumHWThreads}
firstSlider={pendingNumHWThreads.from}
secondSlider={pendingNumHWThreads.to}
inputFieldFrom={pendingNumHWThreads.from}
inputFieldTo={pendingNumHWThreads.to}
/>
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
<DoubleRangeSlider <DoubleRangeSlider
on:change={({ detail }) => { changeRange={(detail) => {
pendingNumAccelerators = { from: detail[0], to: detail[1] }; nodesState.from = detail[0];
isAccsModified = true; nodesState.to = detail[1];
}} }}
min={minNumAccelerators} sliderMin={minNumNodes}
max={maxNumAccelerators} sliderMax={maxNumNodes}
firstSlider={pendingNumAccelerators.from} fromPreset={nodesState.from}
secondSlider={pendingNumAccelerators.to} toPreset={nodesState.to}
inputFieldFrom={pendingNumAccelerators.from}
inputFieldTo={pendingNumAccelerators.to}
/> />
</div>
<div class="mb-3">
<div class="mb-0"><b>Number of HWThreads</b> (Use for Single-Node Jobs)</div>
<DoubleRangeSlider
changeRange={(detail) => {
threadState.from = detail[0];
threadState.to = detail[1];
}}
sliderMin={1}
sliderMax={maxNumHWThreads}
fromPreset={threadState.from}
toPreset={threadState.to}
/>
</div>
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
<div>
<div class="mb-0"><b>Number of Accelerators</b></div>
<DoubleRangeSlider
changeRange={(detail) => {
accState.from = detail[0];
accState.to = detail[1];
}}
sliderMin={0}
sliderMax={maxNumAccelerators}
fromPreset={accState.from}
toPreset={accState.to}
/>
</div>
{/if} {/if}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null} disabled={disableApply}
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
pendingNumNodes = isNodesModified setResources();
? pendingNumNodes setFilter({
: { from: null, to: null }; numNodes: pendingNumNodes,
pendingNumHWThreads = isHwthreadsModified numHWThreads: pendingNumHWThreads,
? pendingNumHWThreads numAccelerators: pendingNumAccelerators,
: { from: null, to: null }; node: pendingNamedNode,
pendingNumAccelerators = isAccsModified nodeMatch: pendingNodeMatch
? pendingNumAccelerators
: { from: null, to: null };
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
numHWThreads = {
from: pendingNumHWThreads.from,
to: pendingNumHWThreads.to,
};
numAccelerators = {
from: pendingNumAccelerators.from,
to: pendingNumAccelerators.to,
};
namedNode = pendingNamedNode;
nodeMatch = pendingNodeMatch;
dispatch("set-filter", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
nodeMatch
}); });
}} }}
> >
@ -247,36 +275,18 @@
</Button> </Button>
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
pendingNumNodes = { from: null, to: null }; resetResources();
pendingNumHWThreads = { from: null, to: null }; setFilter({
pendingNumAccelerators = { from: null, to: null }; numNodes: pendingNumNodes,
pendingNamedNode = null; numHWThreads: pendingNumHWThreads,
pendingNodeMatch = null; numAccelerators: pendingNumAccelerators,
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }; node: pendingNamedNode,
numHWThreads = { nodeMatch: pendingNodeMatch
from: pendingNumHWThreads.from,
to: pendingNumHWThreads.to,
};
numAccelerators = {
from: pendingNumAccelerators.from,
to: pendingNumAccelerators.to,
};
isNodesModified = false;
isHwthreadsModified = false;
isAccsModified = false;
namedNode = pendingNamedNode;
nodeMatch = pendingNodeMatch;
dispatch("set-filter", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
nodeMatch
}); });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -12,8 +12,19 @@
- `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component - `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component
--> -->
<script module>
export const startTimeSelectOptions = [
{ range: "", rangeLabel: "No Selection"},
{ range: "last6h", rangeLabel: "Last 6hrs"},
{ range: "last24h", rangeLabel: "Last 24hrs"},
{ range: "last7d", rangeLabel: "Last 7 days"},
{ range: "last30d", rangeLabel: "Last 30 days"}
];
</script>
<script> <script>
import { createEventDispatcher } from "svelte"; /* Note: Ignore VSCode reported 'A component can only have one instance-level <script> element' error */
import { parse, format, sub } from "date-fns"; import { parse, format, sub } from "date-fns";
import { import {
Row, Row,
@ -26,44 +37,39 @@
FormGroup, FormGroup,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
export let isModified = false; isOpen = $bindable(false),
export let isOpen = false; presetStartTime = { from: null, to: null, range: "" },
export let from = null; setFilter
export let to = null; } = $props();
export let range = "";
export let startTimeSelectOptions;
/* Const Init */
const now = new Date(Date.now()); const now = new Date(Date.now());
const ago = sub(now, { months: 1 }); const ago = sub(now, { months: 1 });
const defaultFrom = { const resetFrom = { date: format(ago, "yyyy-MM-dd"), time: format(ago, "HH:mm")};
date: format(ago, "yyyy-MM-dd"), const resetTo = { date: format(now, "yyyy-MM-dd"), time: format(now, "HH:mm")};
time: format(ago, "HH:mm"),
};
const defaultTo = {
date: format(now, "yyyy-MM-dd"),
time: format(now, "HH:mm"),
};
$: pendingFrom = (from == null) ? defaultFrom : fromRFC3339(from) /* State Init */
$: pendingTo = (to == null) ? defaultTo : fromRFC3339(to) let pendingStartTime = $state(presetStartTime);
$: pendingRange = range let fromState = $state(fromRFC3339(presetStartTime?.from, resetFrom));
let toState = $state(fromRFC3339(presetStartTime?.to, resetTo));
$: isModified = /* Derived Init*/
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) && const rangeSelect = $derived(pendingStartTime?.range ? pendingStartTime.range : "")
(range != pendingRange) &&
!( /* Functions */
from == null && function fromRFC3339(rfc3339, reset) {
pendingFrom.date == "0000-00-00" && if (rfc3339) {
pendingFrom.time == "00:00" const parsedDate = new Date(rfc3339);
) && return {
!( date: format(parsedDate, "yyyy-MM-dd"),
to == null && time: format(parsedDate, "HH:mm"),
pendingTo.date == "0000-00-00" && }
pendingTo.time == "00:00" } else {
) && return reset
!( range == "" && pendingRange == ""); }
}
function toRFC3339({ date, time }, secs = "00") { function toRFC3339({ date, time }, secs = "00") {
const parsedDate = parse( const parsedDate = parse(
@ -73,26 +79,18 @@
); );
return parsedDate.toISOString(); return parsedDate.toISOString();
} }
function fromRFC3339(rfc3339) {
const parsedDate = new Date(rfc3339);
return {
date: format(parsedDate, "yyyy-MM-dd"),
time: format(parsedDate, "HH:mm"),
};
}
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Start Time</ModalHeader> <ModalHeader>Select Start Time</ModalHeader>
<ModalBody> <ModalBody>
{#if range !== ""} {#if rangeSelect !== ""}
<h4>Current Range</h4> <h4>Current Range</h4>
<Row> <Row>
<FormGroup class="col"> <FormGroup class="col">
<Input type ="select" bind:value={pendingRange} > <Input type ="select" bind:value={pendingStartTime.range} >
{#each startTimeSelectOptions as { rangeLabel, range }} {#each startTimeSelectOptions as { rangeLabel, range }}
<option label={rangeLabel} value={range}/> <option label={rangeLabel} value={range}></option>
{/each} {/each}
</Input> </Input>
</FormGroup> </FormGroup>
@ -101,42 +99,41 @@
<h4>From</h4> <h4>From</h4>
<Row> <Row>
<FormGroup class="col"> <FormGroup class="col">
<Input type="date" bind:value={pendingFrom.date} disabled={pendingRange !== ""}/> <Input type="date" bind:value={fromState.date} disabled={rangeSelect !== ""}/>
</FormGroup> </FormGroup>
<FormGroup class="col"> <FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time} disabled={pendingRange !== ""}/> <Input type="time" bind:value={fromState.time} disabled={rangeSelect !== ""}/>
</FormGroup> </FormGroup>
</Row> </Row>
<h4>To</h4> <h4>To</h4>
<Row> <Row>
<FormGroup class="col"> <FormGroup class="col">
<Input type="date" bind:value={pendingTo.date} disabled={pendingRange !== ""}/> <Input type="date" bind:value={toState.date} disabled={rangeSelect !== ""}/>
</FormGroup> </FormGroup>
<FormGroup class="col"> <FormGroup class="col">
<Input type="time" bind:value={pendingTo.time} disabled={pendingRange !== ""}/> <Input type="time" bind:value={toState.time} disabled={rangeSelect !== ""}/>
</FormGroup> </FormGroup>
</Row> </Row>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
{#if pendingRange !== ""} {#if rangeSelect !== ""}
<Button <Button
color="warning" color="warning"
disabled={pendingRange === ""} disabled={rangeSelect === ""}
on:click={() => { onclick={() => {
pendingRange = "" pendingStartTime.range = "";
}} }}
> >
Reset Range Reset Range
</Button> </Button>
<Button <Button
color="primary" color="primary"
disabled={pendingRange === ""} disabled={rangeSelect === ""}
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
from = null; pendingStartTime.from = null;
to = null; pendingStartTime.to = null;
range = pendingRange; setFilter({ startTime: pendingStartTime });
dispatch("set-filter", { from, to, range });
}} }}
> >
Close & Apply Range Close & Apply Range
@ -144,14 +141,14 @@
{:else} {:else}
<Button <Button
color="primary" color="primary"
disabled={pendingFrom.date == "0000-00-00" || disabled={fromState.date == "0000-00-00" ||
pendingTo.date == "0000-00-00"} toState.date == "0000-00-00"}
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
from = toRFC3339(pendingFrom); pendingStartTime.from = toRFC3339(fromState);
to = toRFC3339(pendingTo, "59"); pendingStartTime.to = toRFC3339(toState, "59");
range = ""; pendingStartTime.range = "";
dispatch("set-filter", { from, to, range }); setFilter({ startTime: pendingStartTime });
}} }}
> >
Close & Apply Dates Close & Apply Dates
@ -159,14 +156,16 @@
{/if} {/if}
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
from = null; fromState = resetFrom;
to = null; toState = resetTo;
range = ""; pendingStartTime.from = null;
dispatch("set-filter", { from, to, range }); pendingStartTime.to = null;
pendingStartTime.range = "";
setFilter({ startTime: pendingStartTime });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -2,7 +2,6 @@
@component Filter sub-component for selecting job statistics @component Filter sub-component for selecting job statistics
Properties: Properties:
- `isModified Bool?`: Is this filter component modified [Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false] - `isOpen Bool?`: Is this filter component opened [Default: false]
- `stats [Object]?`: The currently selected statistics filter [Default: []] - `stats [Object]?`: The currently selected statistics filter [Default: []]
@ -11,7 +10,6 @@
--> -->
<script> <script>
import { createEventDispatcher, getContext } from "svelte";
import { getStatsItems } from "../utils.js"; import { getStatsItems } from "../utils.js";
import { import {
Button, Button,
@ -22,75 +20,68 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
const initialized = getContext("initialized"), /* Svelte 5 Props */
dispatch = createEventDispatcher(); let {
isOpen = $bindable(),
presetStats,
setFilter
} = $props();
export let isModified = false; /* Derived Init */
export let isOpen = false; const availableStats = $derived(getStatsItems(presetStats));
export let stats = [];
let statistics = [];
function loadRanges(isInitialized) {
if (!isInitialized) return;
statistics = getStatsItems(stats);
}
/* Functions */
function resetRanges() { function resetRanges() {
for (let st of statistics) { for (let as of availableStats) {
st.enabled = false as.enabled = false
st.from = 0 as.from = 0
st.to = st.peak as.to = as.peak
} };
} }
$: isModified = !statistics.every((a) => {
let b = stats.find((s) => s.field == a.field);
if (b == null) return !a.enabled;
return a.from == b.from && a.to == b.to;
});
$: loadRanges($initialized);
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Filter based on statistics</ModalHeader> <ModalHeader>
<span>Filter based on statistics</span>
</ModalHeader>
<ModalBody> <ModalBody>
{#each statistics as stat} {#each availableStats as aStat}
<h4>{stat.text}</h4> <div class="mb-3">
<DoubleRangeSlider <div class="mb-0"><b>{aStat.text}</b></div>
on:change={({ detail }) => ( <DoubleRangeSlider
(stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true) changeRange={(detail) => {
)} aStat.from = detail[0];
min={0} aStat.to = detail[1];
max={stat.peak} if (aStat.from == 0 && aStat.to == aStat.peak) {
firstSlider={stat.from} aStat.enabled = false;
secondSlider={stat.to} } else {
inputFieldFrom={stat.from} aStat.enabled = true;
inputFieldTo={stat.to} }
/> }}
sliderMin={0.0}
sliderMax={aStat.peak}
fromPreset={aStat.from}
toPreset={aStat.to}
/>
</div>
{/each} {/each}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
stats = statistics.filter((stat) => stat.enabled); setFilter({ stats: [...availableStats.filter((as) => as.enabled)] });
dispatch("set-filter", { stats });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
resetRanges(); resetRanges();
stats = []; setFilter({stats: []});
dispatch("set-filter", { stats });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -11,7 +11,7 @@
--> -->
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { getContext } from "svelte";
import { import {
Button, Button,
ListGroup, ListGroup,
@ -26,20 +26,20 @@
import { fuzzySearchTags } from "../utils.js"; import { fuzzySearchTags } from "../utils.js";
import Tag from "../helper/Tag.svelte"; import Tag from "../helper/Tag.svelte";
const allTags = getContext("tags"), /* Svelte 5 Props */
initialized = getContext("initialized"), let {
dispatch = createEventDispatcher(); isOpen = $bindable(false),
presetTags = [],
setFilter
} = $props();
export let isModified = false; /* Derived */
export let isOpen = false; const allTags = $derived(getContext("tags"))
export let tags = []; const initialized = $derived(getContext("initialized"))
let pendingTags = [...tags]; /* State Init */
$: isModified = let pendingTags = $state(presetTags);
tags.length != pendingTags.length || let searchTerm = $state("");
!tags.every((tagId) => pendingTags.includes(tagId));
let searchTerm = "";
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@ -55,7 +55,7 @@
<Button <Button
outline outline
color="danger" color="danger"
on:click={() => onclick={() =>
(pendingTags = pendingTags.filter((id) => id != tag.id))} (pendingTags = pendingTags.filter((id) => id != tag.id))}
> >
<Icon name="dash-circle" /> <Icon name="dash-circle" />
@ -64,7 +64,7 @@
<Button <Button
outline outline
color="success" color="success"
on:click={() => (pendingTags = [...pendingTags, tag.id])} onclick={() => (pendingTags = [...pendingTags, tag.id])}
> >
<Icon name="plus-circle" /> <Icon name="plus-circle" />
</Button> </Button>
@ -81,21 +81,25 @@
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
tags = [...pendingTags]; setFilter({ tags: [...pendingTags] });
dispatch("set-filter", { tags });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="danger" color="warning"
on:click={() => { onclick={() => {
isOpen = false;
tags = [];
pendingTags = []; pendingTags = [];
dispatch("set-filter", { tags }); }}>Clear Selection</Button
>
<Button
color="danger"
onclick={() => {
isOpen = false;
pendingTags = [];
setFilter({ tags: [...pendingTags] });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>

View File

@ -10,15 +10,7 @@
<script> <script>
import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap"; import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap";
let refreshInterval = $state(null); /* Svelte 5 Props */
let refreshIntervalId = null;
function refreshIntervalChanged() {
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
if (refreshInterval == null) return;
refreshIntervalId = setInterval(() => dispatch("refresh"), refreshInterval);
}
let { let {
initially = null, initially = null,
onRefresh onRefresh
@ -28,6 +20,19 @@
refreshInterval = initially * 1000; refreshInterval = initially * 1000;
refreshIntervalChanged(); refreshIntervalChanged();
} }
/* State Init */
let refreshInterval = $state(null);
/* Var Init */
let refreshIntervalId = null;
/* Functions */
function refreshIntervalChanged() {
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
if (refreshInterval == null) return;
refreshIntervalId = setInterval(() => onRefresh(), refreshInterval);
}
</script> </script>
<InputGroup> <InputGroup>

View File

@ -9,21 +9,30 @@
<script> <script>
import { getContext } from 'svelte' import { getContext } from 'svelte'
const allTags = getContext('tags'),
initialized = getContext('initialized') /* Svelte 5 Props */
let {
id = null,
tag = null,
clickable = true
} = $props();
export let id = null /* Derived */
export let tag = null const allTags = $derived(getContext('tags'));
export let clickable = true const initialized = $derived(getContext('initialized'));
if (tag != null && id == null) /* Effects */
id = tag.id $effect(() => {
if (tag != null && id == null)
id = tag.id
});
$: { $effect(() => {
if ($initialized && tag == null) if ($initialized && tag == null)
tag = allTags.find(tag => tag.id == id) tag = allTags.find(tag => tag.id == id)
} });
/* Function*/
function getScopeColor(scope) { function getScopeColor(scope) {
switch (scope) { switch (scope) {
case "admin": case "admin":

View File

@ -14,7 +14,8 @@
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap"; import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
import { scramble, scrambleNames } from "../utils.js"; import { scramble, scrambleNames } from "../utils.js";
// If page with this component has project preset, keep preset until reset // Note: If page with this component has project preset, keep preset until reset
/* Svelte 5 Props */
let { let {
presetProject = "", presetProject = "",
authlevel = null, authlevel = null,
@ -22,14 +23,20 @@
setFilter setFilter
} = $props(); } = $props();
let mode = $state(presetProject ? "jobName" : "project"); /* Const Init*/
let term = $state(""); const throttle = 500;
/* Var Init */
let user = ""; let user = "";
let project = presetProject ? presetProject : ""; let project = presetProject ? presetProject : "";
let jobName = ""; let jobName = "";
const throttle = 500; let timeoutId = null;
/* State Init */
let mode = $state(presetProject ? "jobName" : "project");
let term = $state("");
/* Functions */
function modeChanged() { function modeChanged() {
if (mode == "user") { if (mode == "user") {
project = presetProject ? presetProject : ""; project = presetProject ? presetProject : "";
@ -44,7 +51,6 @@
termChanged(0); termChanged(0);
} }
let timeoutId = null;
// Compatibility: Handle "user role" and "no role" identically // Compatibility: Handle "user role" and "no role" identically
function termChanged(sleep = throttle) { function termChanged(sleep = throttle) {
if (roles && authlevel >= roles.manager) { if (roles && authlevel >= roles.manager) {

View File

@ -2,6 +2,7 @@
Copyright (c) 2021 Michael Keller Copyright (c) 2021 Michael Keller
Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider) Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider)
Changes: remove dependency, text inputs, configurable value ranges, on:change event Changes: remove dependency, text inputs, configurable value ranges, on:change event
Changes #2: Rewritten for Svelte 5, removed bodyHandler
--> -->
<!-- <!--
@component Selector component to display range selections via min and max double-sliders @component Selector component to display range selections via min and max double-sliders
@ -9,82 +10,80 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
Properties: Properties:
- min: Number - min: Number
- max: Number - max: Number
- firstSlider: Number (Starting position of slider #1) - sliderHandleFrom: Number (Starting position of slider #1)
- secondSlider: Number (Starting position of slider #2) - sliderHandleTo: Number (Starting position of slider #2)
Events: Events:
- `change`: [Number, Number] (Positions of the two sliders) - `change`: [Number, Number] (Positions of the two sliders)
--> -->
<script> <script>
import { createEventDispatcher } from "svelte"; let {
sliderMin,
sliderMax,
fromPreset = 1,
toPreset = 100,
changeRange
} = $props();
export let min; let pendingValues = $state([fromPreset, toPreset]);
export let max; let sliderFrom = $state(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.));
export let firstSlider; let sliderTo = $state(Math.min(((toPreset == null ? sliderMin : toPreset) - sliderMin) / (sliderMax - sliderMin), 1.));
export let secondSlider; let inputFieldFrom = $state(fromPreset.toString());
export let inputFieldFrom = 0; let inputFieldTo = $state(toPreset.toString());
export let inputFieldTo = 0; let leftHandle = $state();
let sliderMain = $state();
const dispatch = createEventDispatcher();
let values;
let start, end; /* Positions of sliders from 0 to 1 */
$: values = [firstSlider, secondSlider]; /* Avoid feedback loop */
$: start = Math.max(((firstSlider == null ? min : firstSlider) - min) / (max - min), 0);
$: end = Math.min(((secondSlider == null ? min : secondSlider) - min) / (max - min), 1);
let leftHandle;
let body;
let slider;
let timeoutId = null; let timeoutId = null;
function queueChangeEvent() { function queueChangeEvent() {
if (timeoutId !== null) { if (timeoutId !== null) {
clearTimeout(timeoutId); clearTimeout(timeoutId)
} }
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null; timeoutId = null
changeRange(pendingValues);
// Show selection but avoid feedback loop }, 100);
if (values[0] != null && inputFieldFrom != values[0].toString())
inputFieldFrom = values[0].toString();
if (values[1] != null && inputFieldTo != values[1].toString())
inputFieldTo = values[1].toString();
dispatch('change', values);
}, 250);
} }
function update() { function updateStates(newValue, newPosition, target) {
values = [ if (target === 'from') {
Math.floor(min + start * (max - min)), pendingValues[0] = isNaN(newValue) ? null : newValue;
Math.floor(min + end * (max - min)) inputFieldFrom = isNaN(newValue) ? null : newValue.toString();
]; sliderFrom = newPosition;
queueChangeEvent(); } else if (target === 'to') {
} pendingValues[1] = isNaN(newValue) ? null : newValue;
inputFieldTo = isNaN(newValue) ? null : newValue.toString();
function inputChanged(idx, event) { sliderTo = newPosition;
let val = Number.parseInt(event.target.value);
if (Number.isNaN(val) || val < min) {
event.target.classList.add('bad');
return;
} }
values[idx] = val;
event.target.classList.remove('bad');
if (idx == 0)
start = clamp((val - min) / (max - min), 0., 1.);
else
end = clamp((val - min) / (max - min), 0., 1.);
queueChangeEvent(); queueChangeEvent();
} }
function clamp(x, min, max) { function rangeChanged (evt, target) {
return x < min evt.preventDefault()
? min evt.stopPropagation()
: (x < max ? x : max); const { left, right } = sliderMain.getBoundingClientRect();
const parentWidth = right - left;
const newP = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
const newV = Math.floor(sliderMin + newP * (sliderMax - sliderMin));
updateStates(newV, newP, target);
}
function inputChanged(evt, target) {
evt.preventDefault()
evt.stopPropagation()
const newV = Number.parseInt(evt.target.value);
const newP = clamp((newV - sliderMin) / (sliderMax - sliderMin), 0., 1.)
updateStates(newV, newP, target);
}
function clamp(x, testMin, testMax) {
return x < testMin
? testMin
: (x > testMax
? testMax
: x
);
} }
function draggable(node) { function draggable(node) {
@ -151,84 +150,38 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
}; };
} }
function setHandlePosition (which) {
return function (evt) {
const { left, right } = slider.getBoundingClientRect();
const parentWidth = right - left;
const p = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
if (which === 'start') {
start = p;
end = Math.max(end, p);
} else {
start = Math.min(p, start);
end = p;
}
update();
}
}
function setHandlesFromBody (evt) {
const { width } = body.getBoundingClientRect();
const { left, right } = slider.getBoundingClientRect();
const parentWidth = right - left;
const leftHandleLeft = leftHandle.getBoundingClientRect().left;
const pxStart = clamp((leftHandleLeft + evt.detail.dx) - left, 0, parentWidth - width);
const pxEnd = clamp(pxStart + width, width, parentWidth);
const pStart = pxStart / parentWidth;
const pEnd = pxEnd / parentWidth;
start = pStart;
end = pEnd;
update();
}
</script> </script>
<div class="double-range-container"> <div class="double-range-container">
<div class="header"> <div class="header">
<input class="form-control" type="text" placeholder="from..." bind:value={inputFieldFrom} <input class="form-control" type="text" placeholder="from..." value={inputFieldFrom}
on:input={(e) => inputChanged(0, e)} /> oninput={(e) => inputChanged(e, 'from')} />
<span>Full Range: <b> {min} </b> - <b> {max} </b></span> <span>Full Range: <b> {sliderMin} </b> - <b> {sliderMax} </b></span>
<input class="form-control" type="text" placeholder="to..." bind:value={inputFieldTo} <input class="form-control" type="text" placeholder="to..." value={inputFieldTo}
on:input={(e) => inputChanged(1, e)} /> oninput={(e) => inputChanged(e, 'to')} />
</div> </div>
<div class="slider" bind:this={slider}>
<div id="slider-active" class="slider" bind:this={sliderMain}>
<div <div
class="body" class="slider-body"
bind:this={body} style="left: {100 * sliderFrom}%;right: {100 * (1 - sliderTo)}%;"
use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlesFromBody}"
style="
left: {100 * start}%;
right: {100 * (1 - end)}%;
"
></div> ></div>
<div <div
class="handle" class="slider-handle"
bind:this={leftHandle} bind:this={leftHandle}
data-which="start" data-which="from"
use:draggable use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('start')}" ondragmove={(e) => rangeChanged(e, 'from')}
style=" style="left: {100 * sliderFrom}%"
left: {100 * start}%
"
></div> ></div>
<div <div
class="handle" class="slider-handle"
data-which="end" data-which="to"
use:draggable use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('end')}" ondragmove={(e) => rangeChanged(e, 'to')}
style=" style="left: {100 * sliderTo}%"
left: {100 * end}%
"
></div> ></div>
</div> </div>
</div> </div>
@ -238,7 +191,8 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: -5px; align-items: flex-end;
margin-bottom: 5px;
} }
.header :nth-child(2) { .header :nth-child(2) {
padding-top: 10px; padding-top: 10px;
@ -249,17 +203,13 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
width: 100px; width: 100px;
} }
:global(.double-range-container .header input[type="text"].bad) {
color: #ff5c33;
border-color: #ff5c33;
}
.double-range-container { .double-range-container {
width: 100%; width: 100%;
height: 50px; height: 50px;
user-select: none; user-select: none;
box-sizing: border-box; box-sizing: border-box;
white-space: nowrap white-space: nowrap;
margin-top: -4px;
} }
.slider { .slider {
position: relative; position: relative;
@ -271,13 +221,13 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c; box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c;
border-radius: 6px; border-radius: 6px;
} }
.handle { .slider-handle {
position: absolute; position: absolute;
top: 50%; top: 50%;
width: 0; width: 0;
height: 0; height: 0;
} }
.handle:after { .slider-handle:after {
content: ' '; content: ' ';
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
@ -291,11 +241,11 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
/* .handle[data-which="end"]:after{ /* .handle[data-which="end"]:after{
transform: translate(-100%, -50%); transform: translate(-100%, -50%);
} */ } */
.handle:active:after { .slider-handle:active:after {
background-color: #ddd; background-color: #ddd;
z-index: 9; z-index: 9;
} }
.body { .slider-body {
top: 0; top: 0;
position: absolute; position: absolute;
background-color: #34a1ff; background-color: #34a1ff;

View File

@ -6,6 +6,12 @@ const headerDomTarget = document.getElementById('svelte-header');
if (headerDomTarget != null) { if (headerDomTarget != null) {
mount(Header, { mount(Header, {
target: headerDomTarget, target: headerDomTarget,
props: { ...header }, props: { // { ...header },
username: hUsername,
authlevel: hAuthlevel,
clusters: hClusters,
subClusters: hSubClusters,
roles: hRoles,
},
}); });
} }

View File

@ -14,13 +14,12 @@
{{block "stylesheets" .}} {{block "stylesheets" .}}
{{end}} {{end}}
<script> <script>
const header = { // Used for header.entrypoint.js mount and filters/Resources.svelte
"username": "{{ .User.Username }}", const hUsername = {{ .User.Username }};
"authlevel": {{ .User.GetAuthLevel }}, const hAuthlevel = {{ .User.GetAuthLevel }};
"clusters": {{ .Clusters }}, const hClusters = {{ .Clusters }};
"subClusters": {{ .SubClusters }}, const hSubClusters = {{ .SubClusters }};
"roles": {{ .Roles }} const hRoles = {{ .Roles }};
};
</script> </script>
</head> </head>
<body class="site"> <body class="site">