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 NavbarTools from "./header/NavbarTools.svelte";
/* Svelte 5 Props */
let { username, authlevel, clusters, subClusters, roles } = $props();
let isOpen = $state(false);
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 Init */
const jobsTitle = new Map();
jobsTitle.set(2, "Job Search");
jobsTitle.set(3, "Managed Jobs");
jobsTitle.set(4, "Jobs");
jobsTitle.set(5, "Jobs");
const usersTitle = new Map();
usersTitle.set(3, "Managed Users");
usersTitle.set(4, "Users");
usersTitle.set(5, "Users");
const projectsTitle = new Map();
projectsTitle.set(3, "Managed Projects");
projectsTitle.set(4, "Projects");
@ -122,6 +118,16 @@
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>
<svelte:window bind:innerWidth={screenSize} />

View File

@ -27,13 +27,15 @@
import Sorting from "./generic/select/SortSelection.svelte";
import MetricSelection from "./generic/select/MetricSelection.svelte";
const { query: initq } = init();
const ccconfig = getContext("cc-config");
// Svelte 5 Props
/* Svelte 5 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 selectedJobs = $state([]);
let filterBuffer = $state([]);
@ -56,20 +58,14 @@
: !!ccconfig.plot_list_showFootprint
);
// Classic Inits
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());
/* Functions */
function resetJobSelection() {
if (filterComponent && selectedJobs.length === 0) {
filterComponent.updateFilters({ dbId: [] });
};
};
/* Reactive Effects */
$effect(() => {
// Reactive : Trigger Effect
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>
<!-- ROW1: Status-->

View File

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

View File

@ -13,7 +13,7 @@
-->
<script>
import { createEventDispatcher, getContext } from "svelte";
import { getContext } from "svelte";
import {
Button,
ListGroup,
@ -24,18 +24,23 @@
ModalFooter,
} from "@sveltestrap/sveltestrap";
const clusters = getContext("clusters"),
initialized = getContext("initialized"),
dispatch = createEventDispatcher();
/* Svelte 5 Props */
let {
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>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@ -45,13 +50,13 @@
<h4>Cluster</h4>
{#if disableClusterSelection}
<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}
<ListGroup>
<ListGroupItem
disabled={disableClusterSelection}
active={pendingCluster == null}
on:click={() => ((pendingCluster = null), (pendingPartition = null))}
onclick={() => ((pendingCluster = null), (pendingPartition = null))}
>
Any Cluster
</ListGroupItem>
@ -59,7 +64,7 @@
<ListGroupItem
disabled={disableClusterSelection}
active={pendingCluster == cluster.name}
on:click={() => (
onclick={() => (
(pendingCluster = cluster.name), (pendingPartition = null)
)}
>
@ -75,14 +80,14 @@
<ListGroup>
<ListGroupItem
active={pendingPartition == null}
on:click={() => (pendingPartition = null)}
onclick={() => (pendingPartition = null)}
>
Any Partition
</ListGroupItem>
{#each clusters.find((c) => c.name == pendingCluster).partitions as partition}
{#each clusters?.find((c) => c.name == pendingCluster)?.partitions as partition}
<ListGroupItem
active={pendingPartition == partition}
on:click={() => (pendingPartition = partition)}
onclick={() => (pendingPartition = partition)}
>
{partition}
</ListGroupItem>
@ -93,22 +98,20 @@
<ModalFooter>
<Button
color="primary"
on:click={() => {
onclick={() => {
isOpen = false;
cluster = pendingCluster;
partition = pendingPartition;
dispatch("set-filter", { cluster, partition });
setFilter({ cluster: pendingCluster, partition: pendingPartition });
}}>Close & Apply</Button
>
<Button
color="danger"
on:click={() => {
onclick={() => {
isOpen = false;
cluster = pendingCluster = null;
partition = pendingPartition = null;
dispatch("set-filter", { cluster, partition });
pendingCluster = null;
pendingPartition = null;
setFilter({ cluster: pendingCluster, partition: pendingPartition})
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
<Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

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

View File

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

View File

@ -4,16 +4,22 @@
Properties:
- `icon String`: Sveltestrap icon name
- `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>
import { Button, Icon } from "@sveltestrap/sveltestrap";
export let icon;
export let modified = false;
/* Svelte 5 Props */
let { icon, modified, onclick, children } = $props();
</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} />
<slot />
{#if children}
<!-- Note: Ignore '@' Error in IDE -->
{@render children()}
{:else}
<span>No content found</span>
{/if}
</Button>

View File

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

View File

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

View File

@ -12,8 +12,19 @@
- `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>
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 {
Row,
@ -26,44 +37,39 @@
FormGroup,
} from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher();
export let isModified = false;
export let isOpen = false;
export let from = null;
export let to = null;
export let range = "";
export let startTimeSelectOptions;
/* Svelte 5 Props */
let {
isOpen = $bindable(false),
presetStartTime = { from: null, to: null, range: "" },
setFilter
} = $props();
/* Const Init */
const now = new Date(Date.now());
const ago = sub(now, { months: 1 });
const defaultFrom = {
date: format(ago, "yyyy-MM-dd"),
time: format(ago, "HH:mm"),
};
const defaultTo = {
date: format(now, "yyyy-MM-dd"),
time: format(now, "HH:mm"),
};
const resetFrom = { date: format(ago, "yyyy-MM-dd"), time: format(ago, "HH:mm")};
const resetTo = { date: format(now, "yyyy-MM-dd"), time: format(now, "HH:mm")};
$: pendingFrom = (from == null) ? defaultFrom : fromRFC3339(from)
$: pendingTo = (to == null) ? defaultTo : fromRFC3339(to)
$: pendingRange = range
/* State Init */
let pendingStartTime = $state(presetStartTime);
let fromState = $state(fromRFC3339(presetStartTime?.from, resetFrom));
let toState = $state(fromRFC3339(presetStartTime?.to, resetTo));
$: isModified =
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) &&
(range != pendingRange) &&
!(
from == null &&
pendingFrom.date == "0000-00-00" &&
pendingFrom.time == "00:00"
) &&
!(
to == null &&
pendingTo.date == "0000-00-00" &&
pendingTo.time == "00:00"
) &&
!( range == "" && pendingRange == "");
/* Derived Init*/
const rangeSelect = $derived(pendingStartTime?.range ? pendingStartTime.range : "")
/* Functions */
function fromRFC3339(rfc3339, reset) {
if (rfc3339) {
const parsedDate = new Date(rfc3339);
return {
date: format(parsedDate, "yyyy-MM-dd"),
time: format(parsedDate, "HH:mm"),
}
} else {
return reset
}
}
function toRFC3339({ date, time }, secs = "00") {
const parsedDate = parse(
@ -73,26 +79,18 @@
);
return parsedDate.toISOString();
}
function fromRFC3339(rfc3339) {
const parsedDate = new Date(rfc3339);
return {
date: format(parsedDate, "yyyy-MM-dd"),
time: format(parsedDate, "HH:mm"),
};
}
</script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Start Time</ModalHeader>
<ModalBody>
{#if range !== ""}
{#if rangeSelect !== ""}
<h4>Current Range</h4>
<Row>
<FormGroup class="col">
<Input type ="select" bind:value={pendingRange} >
<Input type ="select" bind:value={pendingStartTime.range} >
{#each startTimeSelectOptions as { rangeLabel, range }}
<option label={rangeLabel} value={range}/>
<option label={rangeLabel} value={range}></option>
{/each}
</Input>
</FormGroup>
@ -101,42 +99,41 @@
<h4>From</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingFrom.date} disabled={pendingRange !== ""}/>
<Input type="date" bind:value={fromState.date} disabled={rangeSelect !== ""}/>
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time} disabled={pendingRange !== ""}/>
<Input type="time" bind:value={fromState.time} disabled={rangeSelect !== ""}/>
</FormGroup>
</Row>
<h4>To</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingTo.date} disabled={pendingRange !== ""}/>
<Input type="date" bind:value={toState.date} disabled={rangeSelect !== ""}/>
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingTo.time} disabled={pendingRange !== ""}/>
<Input type="time" bind:value={toState.time} disabled={rangeSelect !== ""}/>
</FormGroup>
</Row>
</ModalBody>
<ModalFooter>
{#if pendingRange !== ""}
{#if rangeSelect !== ""}
<Button
color="warning"
disabled={pendingRange === ""}
on:click={() => {
pendingRange = ""
disabled={rangeSelect === ""}
onclick={() => {
pendingStartTime.range = "";
}}
>
Reset Range
</Button>
<Button
color="primary"
disabled={pendingRange === ""}
on:click={() => {
disabled={rangeSelect === ""}
onclick={() => {
isOpen = false;
from = null;
to = null;
range = pendingRange;
dispatch("set-filter", { from, to, range });
pendingStartTime.from = null;
pendingStartTime.to = null;
setFilter({ startTime: pendingStartTime });
}}
>
Close & Apply Range
@ -144,14 +141,14 @@
{:else}
<Button
color="primary"
disabled={pendingFrom.date == "0000-00-00" ||
pendingTo.date == "0000-00-00"}
on:click={() => {
disabled={fromState.date == "0000-00-00" ||
toState.date == "0000-00-00"}
onclick={() => {
isOpen = false;
from = toRFC3339(pendingFrom);
to = toRFC3339(pendingTo, "59");
range = "";
dispatch("set-filter", { from, to, range });
pendingStartTime.from = toRFC3339(fromState);
pendingStartTime.to = toRFC3339(toState, "59");
pendingStartTime.range = "";
setFilter({ startTime: pendingStartTime });
}}
>
Close & Apply Dates
@ -159,14 +156,16 @@
{/if}
<Button
color="danger"
on:click={() => {
onclick={() => {
isOpen = false;
from = null;
to = null;
range = "";
dispatch("set-filter", { from, to, range });
fromState = resetFrom;
toState = resetTo;
pendingStartTime.from = null;
pendingStartTime.to = null;
pendingStartTime.range = "";
setFilter({ startTime: pendingStartTime });
}}>Reset</Button
>
<Button on:click={() => (isOpen = false)}>Close</Button>
<Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
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 {
presetProject = "",
authlevel = null,
@ -22,14 +23,20 @@
setFilter
} = $props();
let mode = $state(presetProject ? "jobName" : "project");
let term = $state("");
/* Const Init*/
const throttle = 500;
/* Var Init */
let user = "";
let project = presetProject ? presetProject : "";
let jobName = "";
const throttle = 500;
let timeoutId = null;
/* State Init */
let mode = $state(presetProject ? "jobName" : "project");
let term = $state("");
/* Functions */
function modeChanged() {
if (mode == "user") {
project = presetProject ? presetProject : "";
@ -44,7 +51,6 @@
termChanged(0);
}
let timeoutId = null;
// Compatibility: Handle "user role" and "no role" identically
function termChanged(sleep = throttle) {
if (roles && authlevel >= roles.manager) {

View File

@ -2,6 +2,7 @@
Copyright (c) 2021 Michael Keller
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 #2: Rewritten for Svelte 5, removed bodyHandler
-->
<!--
@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:
- min: Number
- max: Number
- firstSlider: Number (Starting position of slider #1)
- secondSlider: Number (Starting position of slider #2)
- sliderHandleFrom: Number (Starting position of slider #1)
- sliderHandleTo: Number (Starting position of slider #2)
Events:
- `change`: [Number, Number] (Positions of the two sliders)
-->
<script>
import { createEventDispatcher } from "svelte";
let {
sliderMin,
sliderMax,
fromPreset = 1,
toPreset = 100,
changeRange
} = $props();
export let min;
export let max;
export let firstSlider;
export let secondSlider;
export let inputFieldFrom = 0;
export let inputFieldTo = 0;
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 pendingValues = $state([fromPreset, toPreset]);
let sliderFrom = $state(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.));
let sliderTo = $state(Math.min(((toPreset == null ? sliderMin : toPreset) - sliderMin) / (sliderMax - sliderMin), 1.));
let inputFieldFrom = $state(fromPreset.toString());
let inputFieldTo = $state(toPreset.toString());
let leftHandle = $state();
let sliderMain = $state();
let timeoutId = null;
function queueChangeEvent() {
if (timeoutId !== null) {
clearTimeout(timeoutId);
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
timeoutId = null;
// Show selection but avoid feedback loop
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);
timeoutId = null
changeRange(pendingValues);
}, 100);
}
function update() {
values = [
Math.floor(min + start * (max - min)),
Math.floor(min + end * (max - min))
];
queueChangeEvent();
function updateStates(newValue, newPosition, target) {
if (target === 'from') {
pendingValues[0] = isNaN(newValue) ? null : newValue;
inputFieldFrom = isNaN(newValue) ? null : newValue.toString();
sliderFrom = newPosition;
} else if (target === 'to') {
pendingValues[1] = isNaN(newValue) ? null : newValue;
inputFieldTo = isNaN(newValue) ? null : newValue.toString();
sliderTo = newPosition;
}
function inputChanged(idx, event) {
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();
}
function clamp(x, min, max) {
return x < min
? min
: (x < max ? x : max);
function rangeChanged (evt, target) {
evt.preventDefault()
evt.stopPropagation()
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) {
@ -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>
<div class="double-range-container">
<div class="header">
<input class="form-control" type="text" placeholder="from..." bind:value={inputFieldFrom}
on:input={(e) => inputChanged(0, e)} />
<input class="form-control" type="text" placeholder="from..." value={inputFieldFrom}
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}
on:input={(e) => inputChanged(1, e)} />
<input class="form-control" type="text" placeholder="to..." value={inputFieldTo}
oninput={(e) => inputChanged(e, 'to')} />
</div>
<div class="slider" bind:this={slider}>
<div id="slider-active" class="slider" bind:this={sliderMain}>
<div
class="body"
bind:this={body}
use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlesFromBody}"
style="
left: {100 * start}%;
right: {100 * (1 - end)}%;
"
class="slider-body"
style="left: {100 * sliderFrom}%;right: {100 * (1 - sliderTo)}%;"
></div>
<div
class="handle"
class="slider-handle"
bind:this={leftHandle}
data-which="start"
data-which="from"
use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('start')}"
style="
left: {100 * start}%
"
ondragmove={(e) => rangeChanged(e, 'from')}
style="left: {100 * sliderFrom}%"
></div>
<div
class="handle"
data-which="end"
class="slider-handle"
data-which="to"
use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('end')}"
style="
left: {100 * end}%
"
ondragmove={(e) => rangeChanged(e, 'to')}
style="left: {100 * sliderTo}%"
></div>
</div>
</div>
@ -238,7 +191,8 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: -5px;
align-items: flex-end;
margin-bottom: 5px;
}
.header :nth-child(2) {
padding-top: 10px;
@ -249,17 +203,13 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
width: 100px;
}
:global(.double-range-container .header input[type="text"].bad) {
color: #ff5c33;
border-color: #ff5c33;
}
.double-range-container {
width: 100%;
height: 50px;
user-select: none;
box-sizing: border-box;
white-space: nowrap
white-space: nowrap;
margin-top: -4px;
}
.slider {
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;
border-radius: 6px;
}
.handle {
.slider-handle {
position: absolute;
top: 50%;
width: 0;
height: 0;
}
.handle:after {
.slider-handle:after {
content: ' ';
box-sizing: border-box;
position: absolute;
@ -291,11 +241,11 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
/* .handle[data-which="end"]:after{
transform: translate(-100%, -50%);
} */
.handle:active:after {
.slider-handle:active:after {
background-color: #ddd;
z-index: 9;
}
.body {
.slider-body {
top: 0;
position: absolute;
background-color: #34a1ff;

View File

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

View File

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