mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-06-08 00:23:48 +02:00
Migrate and fix filter component and subcomponents
This commit is contained in:
parent
ffd596e2c7
commit
0b529a5c3c
@ -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} />
|
||||
|
@ -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-->
|
||||
|
@ -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}
|
||||
<Tag id={tagId} clickable={false} />
|
||||
{/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}
|
||||
{/if}
|
||||
{#if isNodesModified && isHwthreadsModified},
|
||||
{/if}
|
||||
{#if isHwthreadsModified}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#if filters.numHWThreads.from != null || filters.numHWThreads.to != null}
|
||||
<Info icon="cpu" onclick={() => (isResourcesOpen = true)}>
|
||||
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
|
||||
{/if}
|
||||
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
|
||||
{/if}
|
||||
{#if isAccsModified}
|
||||
</Info>
|
||||
{/if}
|
||||
|
||||
{#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;
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
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 secsToHoursAndMins(duration) {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
duration -= hours * 3600;
|
||||
const mins = Math.floor(duration / 60);
|
||||
return { hours, mins };
|
||||
function hoursAndMinsToSecs(hoursAndMins) {
|
||||
if (hoursAndMins) {
|
||||
return hoursAndMins.hours * 3600 + hoursAndMins.mins * 60;
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
@ -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>
|
||||
<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}
|
||||
/>
|
||||
<div class="mb-3">
|
||||
<div class="mb-0"><b>Total Job Energy (kWh)</b></div>
|
||||
<DoubleRangeSlider
|
||||
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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => {
|
||||
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>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="mb-0"><b>Number of Nodes</b></div>
|
||||
<DoubleRangeSlider
|
||||
on:change={({ detail }) => {
|
||||
pendingNumAccelerators = { from: detail[0], to: detail[1] };
|
||||
isAccsModified = true;
|
||||
changeRange={(detail) => {
|
||||
nodesState.from = detail[0];
|
||||
nodesState.to = detail[1];
|
||||
}}
|
||||
min={minNumAccelerators}
|
||||
max={maxNumAccelerators}
|
||||
firstSlider={pendingNumAccelerators.from}
|
||||
secondSlider={pendingNumAccelerators.to}
|
||||
inputFieldFrom={pendingNumAccelerators.from}
|
||||
inputFieldTo={pendingNumAccelerators.to}
|
||||
sliderMin={minNumNodes}
|
||||
sliderMax={maxNumNodes}
|
||||
fromPreset={nodesState.from}
|
||||
toPreset={nodesState.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}
|
||||
</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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
<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}
|
||||
/>
|
||||
{#each availableStats as aStat}
|
||||
<div class="mb-3">
|
||||
<div class="mb-0"><b>{aStat.text}</b></div>
|
||||
<DoubleRangeSlider
|
||||
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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
||||
if (tag != null && id == null)
|
||||
id = tag.id
|
||||
/* 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":
|
||||
|
@ -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) {
|
||||
|
@ -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 inputChanged(idx, event) {
|
||||
let val = Number.parseInt(event.target.value);
|
||||
if (Number.isNaN(val) || val < min) {
|
||||
event.target.classList.add('bad');
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user