Merge branch 'migrate_svelte5' into dev

This commit is contained in:
2025-06-24 06:53:18 +02:00
91 changed files with 3511 additions and 3187 deletions
+2 -1
View File
@@ -31,7 +31,8 @@ func (r *clusterResolver) Partitions(ctx context.Context, obj *schema.Cluster) (
// StartTime is the resolver for the startTime field. // StartTime is the resolver for the startTime field.
func (r *jobResolver) StartTime(ctx context.Context, obj *schema.Job) (*time.Time, error) { func (r *jobResolver) StartTime(ctx context.Context, obj *schema.Job) (*time.Time, error) {
panic(fmt.Errorf("not implemented: StartTime - startTime")) timestamp := time.Unix(obj.StartTime, 0)
return &timestamp, nil
} }
// Tags is the resolver for the tags field. // Tags is the resolver for the tags field.
+1 -1
View File
@@ -850,7 +850,7 @@ func (ccms *CCMetricStore) LoadNodeListData(
if len(nodes) > page.ItemsPerPage { if len(nodes) > page.ItemsPerPage {
start := (page.Page - 1) * page.ItemsPerPage start := (page.Page - 1) * page.ItemsPerPage
end := start + page.ItemsPerPage end := start + page.ItemsPerPage
if end > len(nodes) { if end >= len(nodes) {
end = len(nodes) end = len(nodes)
hasNextPage = false hasNextPage = false
} else { } else {
+1 -1
View File
@@ -539,7 +539,7 @@ func (pdb *PrometheusDataRepository) LoadNodeListData(
if len(nodes) > page.ItemsPerPage { if len(nodes) > page.ItemsPerPage {
start := (page.Page - 1) * page.ItemsPerPage start := (page.Page - 1) * page.ItemsPerPage
end := start + page.ItemsPerPage end := start + page.ItemsPerPage
if end > len(nodes) { if end >= len(nodes) {
end = len(nodes) end = len(nodes)
hasNextPage = false hasNextPage = false
} else { } else {
+1 -1
View File
@@ -161,7 +161,7 @@ func setupNodeRoute(i InfoType, r *http.Request) InfoType {
i["hostname"] = vars["hostname"] i["hostname"] = vars["hostname"]
i["id"] = fmt.Sprintf("%s (%s)", vars["cluster"], vars["hostname"]) i["id"] = fmt.Sprintf("%s (%s)", vars["cluster"], vars["hostname"])
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
if from != "" || to != "" { if from != "" && to != "" {
i["from"] = from i["from"] = from
i["to"] = to i["to"] = to
} }
+289 -402
View File
File diff suppressed because it is too large Load Diff
+13 -14
View File
@@ -7,25 +7,24 @@
"dev": "rollup -c -w" "dev": "rollup -c -w"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^25.0.8", "@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@timohausmann/quadtree-js": "^1.2.6", "@timohausmann/quadtree-js": "^1.2.6",
"rollup": "^4.27.4", "rollup": "^4.41.1",
"rollup-plugin-css-only": "^4.5.2", "rollup-plugin-css-only": "^4.5.2",
"rollup-plugin-svelte": "^7.2.2", "rollup-plugin-svelte": "^7.2.2",
"svelte": "^4.2.19" "svelte": "^5.33.14"
}, },
"dependencies": { "dependencies": {
"@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-replace": "^6.0.2",
"@sveltestrap/sveltestrap": "^6.2.7", "@sveltestrap/sveltestrap": "^7.1.0",
"@urql/svelte": "^4.2.2", "@urql/svelte": "^4.2.3",
"chart.js": "^4.4.6", "chart.js": "^4.4.9",
"date-fns": "^2.30.0", "date-fns": "^4.1.0",
"graphql": "^16.9.0", "graphql": "^16.11.0",
"mathjs": "^12.4.3", "mathjs": "^14.5.2",
"svelte-chartjs": "^3.1.5", "uplot": "^1.6.32",
"uplot": "^1.6.31", "wonka": "^6.3.5"
"wonka": "^6.3.4"
} }
} }
+15 -6
View File
@@ -6,13 +6,20 @@ import terser from '@rollup/plugin-terser';
import css from 'rollup-plugin-css-only'; import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH; const production = !process.env.ROLLUP_WATCH;
// const production = false
const plugins = [ const plugins = [
svelte({ svelte({
compilerOptions: { compilerOptions: {
// enable run-time checks when not in production // Enable run-time checks when not in production
dev: !production dev: !production,
// Enable Svelte 5-specific features
hydratable: true, // If using server-side rendering
immutable: true, // Optimize updates for immutable data
// As of sveltestrap 7.1.0, filtered warnings would appear for imported sveltestrap components
warningFilter: (warning) => (
warning.code !== 'element_invalid_self_closing_tag' &&
warning.code !== 'a11y_interactive_supports_focus'
)
} }
}), }),
@@ -23,7 +30,7 @@ const plugins = [
// https://github.com/rollup/plugins/tree/master/packages/commonjs // https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({ resolve({
browser: true, browser: true,
dedupe: ['svelte'] dedupe: ['svelte', '@sveltejs/kit'] // Ensure deduplication for Svelte 5
}), }),
commonjs(), commonjs(),
@@ -32,8 +39,10 @@ const plugins = [
production && terser(), production && terser(),
replace({ replace({
"process.env.NODE_ENV": JSON.stringify("development"), preventAssignment: true,
preventAssignment: true values: {
"process.env.NODE_ENV": JSON.stringify(production ? "production" : "development"),
}
}) })
]; ];
+179 -150
View File
@@ -37,14 +37,12 @@
import ScatterPlot from "./generic/plots/Scatter.svelte"; import ScatterPlot from "./generic/plots/Scatter.svelte";
import RooflineHeatmap from "./generic/plots/RooflineHeatmap.svelte"; import RooflineHeatmap from "./generic/plots/RooflineHeatmap.svelte";
const { query: initq } = init(); /* Svelte 5 Props */
let { filterPresets } = $props();
export let filterPresets;
// By default, look at the jobs of the last 6 hours: // By default, look at the jobs of the last 6 hours:
if (filterPresets?.startTime == null) { if (filterPresets?.startTime == null) {
if (filterPresets == null) filterPresets = {}; if (filterPresets == null) filterPresets = {};
let now = new Date(Date.now()); let now = new Date(Date.now());
let hourAgo = new Date(now); let hourAgo = new Date(now);
hourAgo.setHours(hourAgo.getHours() - 6); hourAgo.setHours(hourAgo.getHours() - 6);
@@ -54,27 +52,10 @@
}; };
} }
let cluster; /* Const Init */
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the const { query: initq } = init();
let jobFilters = []; const client = getContextClient();
let rooflineMaxY;
let colWidth1, colWidth2;
let numBins = 50;
let maxY = -1;
const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics;
$: metrics = [
...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()]),
];
$: clusterName = cluster?.name ? cluster.name : cluster;
const sortOptions = [ const sortOptions = [
{ key: "totalWalltime", label: "Walltime" }, { key: "totalWalltime", label: "Walltime" },
{ key: "totalNodeHours", label: "Node Hours" }, { key: "totalNodeHours", label: "Node Hours" },
@@ -86,7 +67,22 @@
{ key: "project", label: "Project ID" }, { key: "project", label: "Project ID" },
]; ];
let sortSelection = /* Var Init */
let metricUnits = {};
let metricScopes = {};
let numBins = 50;
/* 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 cluster = $state(filterPresets?.cluster);
let rooflineMaxY = $state(0);
let maxY = $state(-1);
let colWidth1 = $state(0);
let colWidth2 = $state(0);
let jobFilters = $state([]);
let metricsInHistograms = $state(ccconfig.analysis_view_histogramMetrics)
let metricsInScatterplots = $state(ccconfig.analysis_view_scatterPlotMetrics)
let sortSelection = $state(
sortOptions.find( sortOptions.find(
(option) => (option) =>
option.key == option.key ==
@@ -94,17 +90,20 @@
) || ) ||
sortOptions.find( sortOptions.find(
(option) => option.key == ccconfig.analysis_view_selectedTopCategory, (option) => option.key == ccconfig.analysis_view_selectedTopCategory,
); )
let groupSelection = );
groupOptions.find( let groupSelection = $state(
(option) => groupOptions.find(
option.key == (option) =>
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`], option.key ==
) || ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`],
groupOptions.find( ) ||
(option) => option.key == ccconfig.analysis_view_selectedTopEntity, groupOptions.find(
); (option) => option.key == ccconfig.analysis_view_selectedTopEntity,
)
);
/* Init Function */
getContext("on-init")(({ data }) => { getContext("on-init")(({ data }) => {
if (data != null) { if (data != null) {
cluster = data.clusters.find((c) => c.name == filterPresets.cluster); cluster = data.clusters.find((c) => c.name == filterPresets.cluster);
@@ -121,120 +120,145 @@
} }
}); });
const client = getContextClient(); /* Derived Vars */
const clusterName = $derived(cluster?.name ? cluster.name : cluster);
const availableMetrics = $derived(loadAvailable($initq?.data?.globalMetrics, clusterName));
const metrics = $derived(
[...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()])]
);
$: statsQuery = queryStore({ let statsQuery = $derived(
client: client, queryStore({
query: gql` client: client,
query ($jobFilters: [JobFilter!]!) { query: gql`
stats: jobsStatistics(filter: $jobFilters) { query ($jobFilters: [JobFilter!]!) {
totalJobs stats: jobsStatistics(filter: $jobFilters) {
shortJobs totalJobs
totalWalltime shortJobs
totalNodeHours totalWalltime
totalCoreHours totalNodeHours
totalAccHours totalCoreHours
histDuration { totalAccHours
count histDuration {
value count
} value
histNumCores { }
count histNumCores {
value count
value
}
} }
} }
} `,
`, variables: { jobFilters },
variables: { jobFilters }, })
}); );
$: topQuery = queryStore({ let topQuery = $derived(
client: client, queryStore({
query: gql` client: client,
query ( query: gql`
$jobFilters: [JobFilter!]! query (
$paging: PageRequest! $jobFilters: [JobFilter!]!
$sortBy: SortByAggregate! $paging: PageRequest!
$groupBy: Aggregate! $sortBy: SortByAggregate!
) { $groupBy: Aggregate!
topList: jobsStatistics(
filter: $jobFilters
page: $paging
sortBy: $sortBy
groupBy: $groupBy
) { ) {
id topList: jobsStatistics(
name filter: $jobFilters
totalWalltime page: $paging
totalNodeHours sortBy: $sortBy
totalCoreHours groupBy: $groupBy
totalAccHours ) {
id
name
totalWalltime
totalNodeHours
totalCoreHours
totalAccHours
}
} }
} `,
`, variables: {
variables: { jobFilters,
jobFilters, paging: { itemsPerPage: 10, page: 1 },
paging: { itemsPerPage: 10, page: 1 }, sortBy: sortSelection.key.toUpperCase(),
sortBy: sortSelection.key.toUpperCase(), groupBy: groupSelection.key.toUpperCase(),
groupBy: groupSelection.key.toUpperCase(), },
}, })
}); );
// Note: Different footprints than those saved in DB per Job -> Caused by Legacy Naming // Note: Different footprints than those saved in DB per Job -> Caused by Legacy Naming
$: footprintsQuery = queryStore({ let footprintsQuery = $derived(
client: client, queryStore({
query: gql` client: client,
query ($jobFilters: [JobFilter!]!, $metrics: [String!]!) { query: gql`
footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) { query ($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
timeWeights { footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
nodeHours timeWeights {
accHours nodeHours
coreHours accHours
} coreHours
metrics { }
metric metrics {
data metric
data
}
} }
} }
} `,
`, variables: { jobFilters, metrics },
variables: { jobFilters, metrics }, })
}); );
let rooflineQuery = $derived(
queryStore({
client: client,
query: gql`
query (
$jobFilters: [JobFilter!]!
$rows: Int!
$cols: Int!
$minX: Float!
$minY: Float!
$maxX: Float!
$maxY: Float!
) {
rooflineHeatmap(
filter: $jobFilters
rows: $rows
cols: $cols
minX: $minX
minY: $minY
maxX: $maxX
maxY: $maxY
)
}
`,
variables: {
jobFilters,
rows: 50,
cols: 50,
minX: 0.01,
minY: 1,
maxX: 1000,
maxY,
},
})
);
$: rooflineQuery = queryStore({ /* Reactive Effects */
client: client, $effect(() => {
query: gql` loadUnitsAndScopes(availableMetrics.length, availableMetrics);
query ( });
$jobFilters: [JobFilter!]! $effect(() => {
$rows: Int! updateEntityConfiguration(groupSelection.key);
$cols: Int! });
$minX: Float! $effect(() => {
$minY: Float! updateCategoryConfiguration(sortSelection.key);
$maxX: Float!
$maxY: Float!
) {
rooflineHeatmap(
filter: $jobFilters
rows: $rows
cols: $cols
minX: $minX
minY: $minY
maxX: $maxX
maxY: $maxY
)
}
`,
variables: {
jobFilters,
rows: 50,
cols: 50,
minX: 0.01,
minY: 1,
maxX: 1000,
maxY,
},
}); });
/* Functions */
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
client: client, client: client,
@@ -287,22 +311,26 @@
} }
} }
let availableMetrics = []; function loadAvailable(globals, name) {
let metricUnits = {}; const availableMetrics = new Set();
let metricScopes = {}; if (globals && globals.length > 0) {
function loadMetrics(isInitialized) { for (let gm of globals) {
if (!isInitialized) return if (gm.availability.find((av) => av.cluster == name)) {
availableMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster.name))] availableMetrics.add({name: gm.name, scope: gm.scope, unit: gm.unit});
for (let sm of availableMetrics) { };
metricUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "") }
metricScopes[sm.name] = sm?.scope }
return [...availableMetrics]
};
function loadUnitsAndScopes(length, available) {
for (let am of available) {
metricUnits[am.name] = (am?.unit?.prefix ? am.unit.prefix : "") + (am?.unit?.base ? am.unit.base : "")
metricScopes[am.name] = am?.scope
} }
} }
$: loadMetrics($initialized) /* On Mount */
$: updateEntityConfiguration(groupSelection.key);
$: updateCategoryConfiguration(sortSelection.key);
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
</script> </script>
@@ -329,7 +357,7 @@
{filterPresets} {filterPresets}
disableClusterSelection={true} disableClusterSelection={true}
startTimeQuickSelect={true} startTimeQuickSelect={true}
on:update-filters={({ detail }) => { applyFilters={(detail) => {
jobFilters = detail.filters; jobFilters = detail.filters;
}} }}
/> />
@@ -392,6 +420,7 @@
<Card body color="danger">{$topQuery.error.message}</Card> <Card body color="danger">{$topQuery.error.message}</Card>
{:else} {:else}
<Pie <Pie
canvasId={`pie-${groupSelection.key}`}
size={colWidth1} size={colWidth1}
sliceLabel={sortSelection.label} sliceLabel={sortSelection.label}
quantities={$topQuery.data.topList.map( quantities={$topQuery.data.topList.map(
+10 -7
View File
@@ -14,15 +14,18 @@
import SupportSettings from "./config/SupportSettings.svelte"; import SupportSettings from "./config/SupportSettings.svelte";
import AdminSettings from "./config/AdminSettings.svelte"; import AdminSettings from "./config/AdminSettings.svelte";
export let isAdmin; /* Svelte 5 Props */
export let isSupport; let {
export let isApi; isAdmin,
export let username; isSupport,
export let ncontent; isApi,
username,
ncontent,
} = $props();
</script> </script>
{#if isAdmin} {#if isAdmin}
<Card style="margin-bottom: 1.5em;"> <Card style="margin-bottom: 1.5rem;">
<CardHeader> <CardHeader>
<CardTitle class="mb-1">Admin Options</CardTitle> <CardTitle class="mb-1">Admin Options</CardTitle>
</CardHeader> </CardHeader>
@@ -31,7 +34,7 @@
{/if} {/if}
{#if isSupport || isAdmin} {#if isSupport || isAdmin}
<Card style="margin-bottom: 1.5em;"> <Card style="margin-bottom: 1.5rem;">
<CardHeader> <CardHeader>
<CardTitle class="mb-1">Support Options</CardTitle> <CardTitle class="mb-1">Support Options</CardTitle>
</CardHeader> </CardHeader>
+28 -14
View File
@@ -5,6 +5,7 @@
- `username String`: Empty string if auth. is disabled, otherwise the username as string - `username String`: Empty string if auth. is disabled, otherwise the username as string
- `authlevel Number`: The current users authentication level - `authlevel Number`: The current users authentication level
- `clusters [String]`: List of cluster names - `clusters [String]`: List of cluster names
- `subClusters [String]`: List of subCluster names
- `roles [Number]`: Enum containing available roles - `roles [Number]`: Enum containing available roles
--> -->
@@ -23,24 +24,21 @@
import NavbarLinks from "./header/NavbarLinks.svelte"; import NavbarLinks from "./header/NavbarLinks.svelte";
import NavbarTools from "./header/NavbarTools.svelte"; import NavbarTools from "./header/NavbarTools.svelte";
export let username; /* Svelte 5 Props */
export let authlevel; let { username, authlevel, clusters, subClusters, roles } = $props();
export let clusters;
export let subClusters;
export let roles;
let isOpen = false;
let screenSize;
/* Const Init */
const jobsTitle = new Map(); const jobsTitle = new Map();
jobsTitle.set(2, "Job Search"); jobsTitle.set(2, "Job Search");
jobsTitle.set(3, "Managed Jobs"); jobsTitle.set(3, "Managed Jobs");
jobsTitle.set(4, "Jobs"); jobsTitle.set(4, "Jobs");
jobsTitle.set(5, "Jobs"); jobsTitle.set(5, "Jobs");
const usersTitle = new Map(); const usersTitle = new Map();
usersTitle.set(3, "Managed Users"); usersTitle.set(3, "Managed Users");
usersTitle.set(4, "Users"); usersTitle.set(4, "Users");
usersTitle.set(5, "Users"); usersTitle.set(5, "Users");
const projectsTitle = new Map(); const projectsTitle = new Map();
projectsTitle.set(3, "Managed Projects"); projectsTitle.set(3, "Managed Projects");
projectsTitle.set(4, "Projects"); projectsTitle.set(4, "Projects");
@@ -120,29 +118,41 @@
menu: "Info", menu: "Info",
}, },
]; ];
/* State Init */
let isOpen = $state(false);
let screenSize = $state(0);
/* Derived Vars */
let showMax = $derived(screenSize >= 1500);
let showMid = $derived(screenSize < 1500 && screenSize >= 1300);
let showSml = $derived(screenSize < 1300 && screenSize >= 768);
let showBrg = $derived(screenSize < 768);
</script> </script>
<svelte:window bind:innerWidth={screenSize} /> <svelte:window bind:innerWidth={screenSize} />
<Navbar color="light" light expand="md" fixed="top"> <Navbar color="light" light expand="md" fixed="top">
<NavbarBrand href="/"> <NavbarBrand href="/">
<img alt="ClusterCockpit Logo" src="/img/logo.png" height="25rem" /> <img alt="ClusterCockpit Logo" src="/img/logo.png" height="25rem" />
</NavbarBrand> </NavbarBrand>
<NavbarToggler on:click={() => (isOpen = !isOpen)} /> <NavbarToggler onclick={() => (isOpen = !isOpen)} />
<Collapse <Collapse
style="justify-content: space-between" style="justify-content: space-between"
{isOpen} {isOpen}
navbar navbar
expand="md" expand="md"
on:update={({ detail }) => (isOpen = detail.isOpen)} onupdate={({ detail }) => (isOpen = detail.isOpen)}
> >
<Nav navbar> <Nav navbar>
{#if screenSize > 1500 || screenSize < 768} {#if showMax || showBrg}
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters} {subClusters}
links={views.filter((item) => item.requiredRole <= authlevel)} links={views.filter((item) => item.requiredRole <= authlevel)}
/> />
{:else if screenSize > 1300}
{:else if showMid}
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters} {subClusters}
@@ -169,7 +179,8 @@
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
{/if} {/if}
{:else}
{:else if showSml}
<NavbarLinks <NavbarLinks
{clusters} {clusters}
{subClusters} {subClusters}
@@ -228,8 +239,11 @@
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
{/if} {/if}
{:else}
<span>Error: Unknown Window Size!</span>
{/if} {/if}
</Nav> </Nav>
<NavbarTools {username} {authlevel} {roles} {screenSize} /> <NavbarTools {username} {authlevel} {roles} {screenSize} />
</Collapse> </Collapse>
</Navbar> </Navbar>
+10 -6
View File
@@ -56,8 +56,8 @@
selectedScopes = [], selectedScopes = [],
plots = {}; plots = {};
let availableMetrics = new Set(), let totalMetrics = 0;
missingMetrics = [], let missingMetrics = [],
missingHosts = [], missingHosts = [],
somethingMissing = false; somethingMissing = false;
@@ -294,7 +294,7 @@
{#if $initq?.data} {#if $initq?.data}
<Col xs="auto"> <Col xs="auto">
<Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary"> <Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary">
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available) Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
</Button> </Button>
</Col> </Col>
{/if} {/if}
@@ -428,12 +428,16 @@
{#if $initq?.data} {#if $initq?.data}
<MetricSelection <MetricSelection
bind:isOpen={isMetricsSelectionOpen}
bind:totalMetrics
presetMetrics={selectedMetrics}
cluster={$initq.data.job.cluster} cluster={$initq.data.job.cluster}
subCluster={$initq.data.job.subCluster} subCluster={$initq.data.job.subCluster}
configName="job_view_selectedMetrics" configName="job_view_selectedMetrics"
bind:metrics={selectedMetrics} preInitialized
bind:isOpen={isMetricsSelectionOpen} applyMetrics={(newMetrics) =>
bind:allMetrics={availableMetrics} selectedMetrics = [...newMetrics]
}
/> />
{/if} {/if}
+76 -50
View File
@@ -8,7 +8,7 @@
--> -->
<script> <script>
import { onMount, getContext } from "svelte"; import { untrack, onMount, getContext } from "svelte";
import { import {
Row, Row,
Col, Col,
@@ -27,43 +27,59 @@
import Sorting from "./generic/select/SortSelection.svelte"; import Sorting from "./generic/select/SortSelection.svelte";
import MetricSelection from "./generic/select/MetricSelection.svelte"; import MetricSelection from "./generic/select/MetricSelection.svelte";
/* Svelte 5 Props */
let { filterPresets, authlevel, roles } = $props();
/* Const Init */
const { query: initq } = init(); const { query: initq } = init();
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
const presetProject = filterPresets?.project ? filterPresets.project : ""
export let filterPresets = {}; /* State Init */
export let authlevel; 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
export let roles; let selectedJobs = $state([]);
let filterBuffer = $state([]);
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the let jobList = $state(null);
let filterBuffer = []; let jobCompare = $state(null);
let selectedJobs = []; let matchedListJobs = $state(0);
let jobList, let matchedCompareJobs = $state(0);
jobCompare, let isSortingOpen = $state(false);
matchedListJobs, let showCompare = $state(false);
matchedCompareJobs = null; let isMetricsSelectionOpen = $state(false);
let sorting = { field: "startTime", type: "col", order: "DESC" }, let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
isSortingOpen = false, let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
isMetricsSelectionOpen = false; let metrics = $state(filterPresets.cluster
let metrics = filterPresets.cluster
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
ccconfig.plot_list_selectedMetrics ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics; : ccconfig.plot_list_selectedMetrics
let showFootprint = filterPresets.cluster );
let showFootprint = $state(filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`] ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint; : !!ccconfig.plot_list_showFootprint
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null; );
let presetProject = filterPresets?.project ? filterPresets.project : ""
let showCompare = false;
/* Functions */
function resetJobSelection() {
if (filterComponent && selectedJobs.length === 0) {
filterComponent.updateFilters({ dbId: [] });
};
};
/* Reactive Effects */
$effect(() => {
// Reactive : Trigger Effect
selectedJobs.length
untrack(() => {
// Unreactive : Apply Reset w/o starting infinite loop
resetJobSelection()
});
});
/* On Mount */
// The filterPresets are handled by the Filters component, // The filterPresets are handled by the Filters component,
// so we need to wait for it to be ready before we can start a query. // so we need to wait for it to be ready before we can start a query.
// This is also why JobList component starts out with a paused query. // This is also why JobList component starts out with a paused query.
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
$: if (filterComponent && selectedJobs.length == 0) {
filterComponent.updateFilters({dbId: []})
}
</script> </script>
<!-- ROW1: Status--> <!-- ROW1: Status-->
@@ -85,25 +101,25 @@
<Row cols={{ xs: 1, md: 2, lg: 5}} class="mb-3"> <Row cols={{ xs: 1, md: 2, lg: 5}} class="mb-3">
<Col lg="2" class="mb-2 mb-lg-0"> <Col lg="2" class="mb-2 mb-lg-0">
<ButtonGroup class="w-100"> <ButtonGroup class="w-100">
<Button outline color="primary" on:click={() => (isSortingOpen = true)} disabled={showCompare}> <Button outline color="primary" onclick={() => (isSortingOpen = true)} disabled={showCompare}>
<Icon name="sort-up" /> Sorting <Icon name="sort-up" /> Sorting
</Button> </Button>
<Button <Button
outline outline
color="primary" color="primary"
on:click={() => (isMetricsSelectionOpen = true)} onclick={() => (isMetricsSelectionOpen = true)}
> >
<Icon name="graph-up" /> Metrics <Icon name="graph-up" /> Metrics
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</Col> </Col>
<Col lg="4" class="mb-1 mb-lg-0"> <Col lg="5" class="mb-1 mb-lg-0">
<Filters <Filters
showFilter={!showCompare}
{filterPresets}
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
bind:this={filterComponent} bind:this={filterComponent}
on:update-filters={({ detail }) => { {filterPresets}
showFilter={!showCompare}
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
applyFilters={(detail) => {
selectedCluster = detail.filters[0]?.cluster selectedCluster = detail.filters[0]?.cluster
? detail.filters[0].cluster.eq ? detail.filters[0].cluster.eq
: null; : null;
@@ -122,29 +138,31 @@
{presetProject} {presetProject}
bind:authlevel bind:authlevel
bind:roles bind:roles
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)} setFilter={(filter) => filterComponent.updateFilters(filter)}
/> />
{/if} {/if}
</Col> </Col>
<Col lg="2" class="mb-1 mb-lg-0"> <Col lg="3" class="mb-1 mb-lg-0 d-inline-flex align-items-start justify-content-end ">
{#if !showCompare} {#if !showCompare}
<Refresher on:refresh={() => { <Refresher presetClass="w-auto" onRefresh={() => {
jobList.refreshJobs() jobList.refreshJobs()
jobList.refreshAllMetrics() jobList.refreshAllMetrics()
}} /> }} />
{/if} {/if}
</Col> <div class="mx-1"></div>
<Col lg="2" class="mb-2 mb-lg-0"> <ButtonGroup class="w-50">
<ButtonGroup class="w-100"> <Button color="primary" disabled={(matchedListJobs >= 500 && !(selectedJobs.length != 0)) || $initq.fetching} onclick={() => {
<Button color="primary" disabled={matchedListJobs >= 500 && !(selectedJobs.length != 0)} on:click={() => {
if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs}, true) if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs}, true)
showCompare = !showCompare showCompare = !showCompare
}} > }} >
{showCompare ? 'Return to List' : {showCompare ? 'Return to List' :
'Compare Jobs' + (selectedJobs.length != 0 ? ` (${selectedJobs.length} selected)` : matchedListJobs >= 500 ? ` (Too Many)` : ``)} matchedListJobs >= 500 && selectedJobs.length == 0
? 'Compare Disabled'
: 'Compare' + (selectedJobs.length != 0 ? ` ${selectedJobs.length} ` : ' ') + 'Jobs'
}
</Button> </Button>
{#if !showCompare && selectedJobs.length != 0} {#if !showCompare && selectedJobs.length != 0}
<Button color="warning" on:click={() => { <Button class="w-auto" color="warning" onclick={() => {
selectedJobs = [] // Only empty array, filters handled by reactive reset selectedJobs = [] // Only empty array, filters handled by reactive reset
}}> }}>
Clear Clear
@@ -178,13 +196,21 @@
</Col> </Col>
</Row> </Row>
<Sorting bind:sorting bind:isOpen={isSortingOpen}/> <Sorting
bind:isOpen={isSortingOpen}
presetSorting={sorting}
applySorting={(newSort) =>
sorting = {...newSort}
}/>
<MetricSelection <MetricSelection
bind:cluster={selectedCluster} bind:isOpen={isMetricsSelectionOpen}
configName="plot_list_selectedMetrics" bind:showFootprint
bind:metrics presetMetrics={metrics}
bind:isOpen={isMetricsSelectionOpen} cluster={selectedCluster}
bind:showFootprint configName="plot_list_selectedMetrics"
footprintSelect footprintSelect
applyMetrics={(newMetrics) =>
metrics = [...newMetrics]
}
/> />
+90 -52
View File
@@ -31,10 +31,8 @@
} from "./generic/utils.js"; } from "./generic/utils.js";
import Filters from "./generic/Filters.svelte"; import Filters from "./generic/Filters.svelte";
const {} = init(); /* Svelte 5 Props */
let { type, filterPresets } = $props();
export let type;
export let filterPresets;
// By default, look at the jobs of the last 30 days: // By default, look at the jobs of the last 30 days:
if (filterPresets?.startTime == null) { if (filterPresets?.startTime == null) {
@@ -51,50 +49,64 @@
"Invalid list type provided!", "Invalid list type provided!",
); );
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the /* Const Init */
let jobFilters = []; const {} = init();
let nameFilter = "";
let sorting = { field: "totalJobs", direction: "down" };
const client = getContextClient(); const client = getContextClient();
$: stats = queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!) {
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
id
name
totalJobs
totalWalltime
totalCoreHours
totalAccHours
}
}`,
variables: { jobFilters },
});
function changeSorting(event, field) { /* State Init*/
let target = event.target; 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
while (target.tagName != "BUTTON") target = target.parentElement; let jobFilters = $state([]);
let nameFilter = $state("");
let sorting = $state({ field: "totalJobs", direction: "down" });
let direction = target.children[0].className.includes("up") ? "down" : "up"; /* Derived Vars */
target.children[0].className = `bi-sort-numeric-${direction}`; let stats = $derived(
sorting = { field, direction }; queryStore({
client: client,
query: gql`
query($jobFilters: [JobFilter!]!) {
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
id
name
totalJobs
totalWalltime
totalCoreHours
totalAccHours
}
}`,
variables: { jobFilters },
})
);
/* Functions */
function changeSorting(field) {
sorting = { field, direction: sorting?.direction == "down" ? "up" : "down" };
} }
function sort(stats, sorting, nameFilter) { function sort(stats, sorting, nameFilter) {
const cmp = const idCmp = sorting.direction == "up"
sorting.field == "id" ? (a, b) => b.id.localeCompare(a.id)
? sorting.direction == "up" : (a, b) => a.id.localeCompare(b.id)
? (a, b) => a.id < b.id
: (a, b) => a.id > b.id
: sorting.direction == "up"
? (a, b) => a[sorting.field] - b[sorting.field]
: (a, b) => b[sorting.field] - a[sorting.field];
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp); // Force empty or undefined strings to the end of the list
const nameCmp = sorting.direction == "up"
? (a, b) => !a?.name ? 1 : (!b?.name ? -1 : (b.name.localeCompare(a.name)))
: (a, b) => !a?.name ? 1 : (!b?.name ? -1 : (a.name.localeCompare(b.name)))
const intCmp = sorting.direction == "up"
? (a, b) => a[sorting.field] - b[sorting.field]
: (a, b) => b[sorting.field] - a[sorting.field];
if (sorting.field == "id") {
return stats.filter((u) => u.id.includes(nameFilter)).sort(idCmp)
} else if (sorting.field == "name") {
return stats.filter((u) => u.id.includes(nameFilter)).sort(nameCmp)
} else {
return stats.filter((u) => u.id.includes(nameFilter)).sort(intCmp)
}
} }
/* On Mount */
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
</script> </script>
@@ -119,7 +131,7 @@
{filterPresets} {filterPresets}
startTimeQuickSelect={true} startTimeQuickSelect={true}
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up" menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
on:update-filters={({ detail }) => { applyFilters={(detail) => {
jobFilters = detail.filters; jobFilters = detail.filters;
}} }}
/> />
@@ -137,9 +149,14 @@
<Button <Button
color={sorting.field == "id" ? "primary" : "light"} color={sorting.field == "id" ? "primary" : "light"}
size="sm" size="sm"
on:click={(e) => changeSorting(e, "id")} onclick={() => changeSorting("id")}
> >
<Icon name="sort-numeric-down" /> {#if sorting?.field == "id"}
<!-- Note on Icon-Name: Arrow-indicator always down, only alpha-indicator switches -->
<Icon name={`sort-alpha-${sorting?.direction == 'down' ? 'down' : 'down-alt'}`} />
{:else}
<Icon name="three-dots-vertical" />
{/if}
</Button> </Button>
</th> </th>
{#if type == "USER"} {#if type == "USER"}
@@ -148,9 +165,13 @@
<Button <Button
color={sorting.field == "name" ? "primary" : "light"} color={sorting.field == "name" ? "primary" : "light"}
size="sm" size="sm"
on:click={(e) => changeSorting(e, "name")} onclick={() => changeSorting("name")}
> >
<Icon name="sort-numeric-down" /> {#if sorting?.field == "name"}
<Icon name={`sort-alpha-${sorting?.direction == 'down' ? 'down' : 'down-alt'}`} />
{:else}
<Icon name="three-dots-vertical" />
{/if}
</Button> </Button>
</th> </th>
{/if} {/if}
@@ -159,9 +180,14 @@
<Button <Button
color={sorting.field == "totalJobs" ? "primary" : "light"} color={sorting.field == "totalJobs" ? "primary" : "light"}
size="sm" size="sm"
on:click={(e) => changeSorting(e, "totalJobs")} onclick={() => changeSorting("totalJobs")}
> >
<Icon name="sort-numeric-down" /> {#if sorting?.field == "totalJobs"}
<!-- Note on Icon-Name: Arrow-indicator always down, only numeric-indicator switches -->
<Icon name={`sort-numeric-${sorting?.direction == 'down' ? 'down-alt' : 'down'}`} />
{:else}
<Icon name="three-dots-vertical" />
{/if}
</Button> </Button>
</th> </th>
<th scope="col"> <th scope="col">
@@ -169,9 +195,13 @@
<Button <Button
color={sorting.field == "totalWalltime" ? "primary" : "light"} color={sorting.field == "totalWalltime" ? "primary" : "light"}
size="sm" size="sm"
on:click={(e) => changeSorting(e, "totalWalltime")} onclick={() => changeSorting("totalWalltime")}
> >
<Icon name="sort-numeric-down" /> {#if sorting?.field == "totalWalltime"}
<Icon name={`sort-numeric-${sorting?.direction == 'down' ? 'down-alt' : 'down'}`} />
{:else}
<Icon name="three-dots-vertical" />
{/if}
</Button> </Button>
</th> </th>
<th scope="col"> <th scope="col">
@@ -179,9 +209,13 @@
<Button <Button
color={sorting.field == "totalCoreHours" ? "primary" : "light"} color={sorting.field == "totalCoreHours" ? "primary" : "light"}
size="sm" size="sm"
on:click={(e) => changeSorting(e, "totalCoreHours")} onclick={() => changeSorting("totalCoreHours")}
> >
<Icon name="sort-numeric-down" /> {#if sorting?.field == "totalCoreHours"}
<Icon name={`sort-numeric-${sorting?.direction == 'down' ? 'down-alt' : 'down'}`} />
{:else}
<Icon name="three-dots-vertical" />
{/if}
</Button> </Button>
</th> </th>
<th scope="col"> <th scope="col">
@@ -189,9 +223,13 @@
<Button <Button
color={sorting.field == "totalAccHours" ? "primary" : "light"} color={sorting.field == "totalAccHours" ? "primary" : "light"}
size="sm" size="sm"
on:click={(e) => changeSorting(e, "totalAccHours")} onclick={() => changeSorting("totalAccHours")}
> >
<Icon name="sort-numeric-down" /> {#if sorting?.field == "totalAccHours"}
<Icon name={`sort-numeric-${sorting?.direction == 'down' ? 'down-alt' : 'down'}`} />
{:else}
<Icon name="three-dots-vertical" />
{/if}
</Button> </Button>
</th> </th>
</tr> </tr>
+57 -42
View File
@@ -9,7 +9,9 @@
--> -->
<script> <script>
import { getContext } from "svelte"; import {
getContext,
} from "svelte";
import { import {
Row, Row,
Col, Col,
@@ -34,23 +36,28 @@
import TimeSelection from "./generic/select/TimeSelection.svelte"; import TimeSelection from "./generic/select/TimeSelection.svelte";
import Refresher from "./generic/helper/Refresher.svelte"; import Refresher from "./generic/helper/Refresher.svelte";
export let cluster; /* Svelte 5 Props */
export let hostname; let {
export let from = null; cluster,
export let to = null; hostname,
presetFrom = null,
presetTo = null,
} = $props();
/* Const Init */
const { query: initq } = init(); const { query: initq } = init();
if (from == null || to == null) {
to = new Date(Date.now());
from = new Date(to.getTime());
from.setHours(from.getHours() - 4);
}
const initialized = getContext("initialized") const initialized = getContext("initialized")
const globalMetrics = getContext("globalMetrics") const globalMetrics = getContext("globalMetrics")
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
const clusters = getContext("clusters"); const clusters = getContext("clusters");
const nowEpoch = Date.now();
const paging = { itemsPerPage: 50, page: 1 };
const sorting = { field: "startTime", type: "col", order: "DESC" };
const filter = [
{ cluster: { eq: cluster } },
{ node: { contains: hostname } },
{ state: ["running"] },
];
const client = getContextClient(); const client = getContextClient();
const nodeMetricsQuery = gql` const nodeMetricsQuery = gql`
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) { query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
@@ -79,27 +86,6 @@
} }
} }
`; `;
$: nodeMetricsData = queryStore({
client: client,
query: nodeMetricsQuery,
variables: {
cluster: cluster,
nodes: [hostname],
from: from.toISOString(),
to: to.toISOString(),
},
});
const paging = { itemsPerPage: 50, page: 1 };
const sorting = { field: "startTime", type: "col", order: "DESC" };
const filter = [
{ cluster: { eq: cluster } },
{ node: { contains: hostname } },
{ state: ["running"] },
];
const nodeJobsQuery = gql` const nodeJobsQuery = gql`
query ( query (
$filter: [JobFilter!]! $filter: [JobFilter!]!
@@ -112,13 +98,37 @@
} }
`; `;
$: nodeJobsData = queryStore({ /* State Init */
client: client, let from = $state(presetFrom ? presetFrom : new Date(nowEpoch - (4 * 3600 * 1000)));
query: nodeJobsQuery, let to = $state(presetTo ? presetTo : new Date(nowEpoch));
variables: { paging, sorting, filter }, let systemUnits = $state({});
/* Derived */
const nodeMetricsData = $derived(queryStore({
client: client,
query: nodeMetricsQuery,
variables: {
cluster: cluster,
nodes: [hostname],
from: from?.toISOString(),
to: to?.toISOString(),
},
})
);
const nodeJobsData = $derived(queryStore({
client: client,
query: nodeJobsQuery,
variables: { paging, sorting, filter },
})
);
/* Effect */
$effect(() => {
loadUnits($initialized);
}); });
let systemUnits = {}; /* Functions */
function loadUnits(isInitialized) { function loadUnits(isInitialized) {
if (!isInitialized) return if (!isInitialized) return
const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
@@ -126,8 +136,6 @@
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "") systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
} }
} }
$: loadUnits($initialized)
</script> </script>
<Row cols={{ xs: 2, lg: 4 }}> <Row cols={{ xs: 2, lg: 4 }}>
@@ -146,7 +154,14 @@
</Col> </Col>
<!-- Time Col --> <!-- Time Col -->
<Col> <Col>
<TimeSelection bind:from bind:to /> <TimeSelection
presetFrom={from}
presetTo={to}
applyTime={(newFrom, newTo) => {
from = newFrom;
to = newTo;
}}
/>
</Col> </Col>
<!-- Concurrent Col --> <!-- Concurrent Col -->
<Col class="mt-2 mt-lg-0"> <Col class="mt-2 mt-lg-0">
@@ -172,7 +187,7 @@
<!-- Refresh Col--> <!-- Refresh Col-->
<Col class="mt-2 mt-lg-0"> <Col class="mt-2 mt-lg-0">
<Refresher <Refresher
on:refresh={() => { onRefresh={() => {
const diff = Date.now() - to; const diff = Date.now() - to;
from = new Date(from.getTime() + diff); from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff); to = new Date(to.getTime() + diff);
+88 -73
View File
@@ -42,15 +42,14 @@
import Refresher from "./generic/helper/Refresher.svelte"; import Refresher from "./generic/helper/Refresher.svelte";
import HistogramSelection from "./generic/select/HistogramSelection.svelte"; import HistogramSelection from "./generic/select/HistogramSelection.svelte";
/* Svelte 5 Props */
let { cluster } = $props();
/* Const Init */
const { query: initq } = init(); const { query: initq } = init();
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
const client = getContextClient();
export let cluster; const paging = { itemsPerPage: 10, page: 1 }; // Top 10
let plotWidths = [];
let colWidth;
let from = new Date(Date.now() - 5 * 60 * 1000),
to = new Date(Date.now());
const topOptions = [ const topOptions = [
{ key: "totalJobs", label: "Jobs" }, { key: "totalJobs", label: "Jobs" },
{ key: "totalNodes", label: "Nodes" }, { key: "totalNodes", label: "Nodes" },
@@ -58,7 +57,26 @@
{ key: "totalAccs", label: "Accelerators" }, { key: "totalAccs", label: "Accelerators" },
]; ];
let topProjectSelection = /* State Init */
let from = $state(new Date(Date.now() - 5 * 60 * 1000));
let to = $state(new Date(Date.now()));
let isHistogramSelectionOpen = $state(false);
let colWidth = $state(0);
let plotWidths = $state([]);
// Bar Gauges
let allocatedNodes = $state({});
let flopRate = $state({});
let flopRateUnitPrefix = $state({});
let flopRateUnitBase = $state({});
let memBwRate = $state({});
let memBwRateUnitPrefix = $state({});
let memBwRateUnitBase = $state({});
let selectedHistograms = $state(cluster
? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig['user_view_histogramMetrics'] || []);
let topProjectSelection = $state(
topOptions.find( topOptions.find(
(option) => (option) =>
option.key == option.key ==
@@ -66,8 +84,10 @@
) || ) ||
topOptions.find( topOptions.find(
(option) => option.key == ccconfig.status_view_selectedTopProjectCategory, (option) => option.key == ccconfig.status_view_selectedTopProjectCategory,
); )
let topUserSelection = );
let topUserSelection = $state(
topOptions.find( topOptions.find(
(option) => (option) =>
option.key == option.key ==
@@ -75,16 +95,12 @@
) || ) ||
topOptions.find( topOptions.find(
(option) => option.key == ccconfig.status_view_selectedTopUserCategory, (option) => option.key == ccconfig.status_view_selectedTopUserCategory,
); )
);
let isHistogramSelectionOpen = false; /* Derived */
$: selectedHistograms = cluster
? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig['user_view_histogramMetrics'] || [];
const client = getContextClient();
// Note: nodeMetrics are requested on configured $timestep resolution // Note: nodeMetrics are requested on configured $timestep resolution
$: mainQuery = queryStore({ const mainQuery = $derived(queryStore({
client: client, client: client,
query: gql` query: gql`
query ( query (
@@ -162,10 +178,9 @@
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
selectedHistograms: selectedHistograms, selectedHistograms: selectedHistograms,
}, },
}); }));
const paging = { itemsPerPage: 10, page: 1 }; // Top 10 const topUserQuery = $derived(queryStore({
$: topUserQuery = queryStore({
client: client, client: client,
query: gql` query: gql`
query ( query (
@@ -193,9 +208,9 @@
paging, paging,
sortBy: topUserSelection.key.toUpperCase(), sortBy: topUserSelection.key.toUpperCase(),
}, },
}); }));
$: topProjectQuery = queryStore({ const topProjectQuery = $derived(queryStore({
client: client, client: client,
query: gql` query: gql`
query ( query (
@@ -222,8 +237,46 @@
paging, paging,
sortBy: topProjectSelection.key.toUpperCase(), sortBy: topProjectSelection.key.toUpperCase(),
}, },
}));
/* Effects */
$effect(() => {
if ($initq.data && $mainQuery.data) {
let subClusters = $initq.data.clusters.find(
(c) => c.name == cluster,
).subClusters;
for (let subCluster of subClusters) {
allocatedNodes[subCluster.name] =
$mainQuery.data.allocatedNodes.find(
({ name }) => name == subCluster.name,
)?.count || 0;
flopRate[subCluster.name] =
Math.floor(
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "flops_any") *
100,
) / 100;
flopRateUnitPrefix[subCluster.name] = subCluster.flopRateSimd.unit.prefix;
flopRateUnitBase[subCluster.name] = subCluster.flopRateSimd.unit.base;
memBwRate[subCluster.name] =
Math.floor(
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "mem_bw") * 100,
) / 100;
memBwRateUnitPrefix[subCluster.name] =
subCluster.memoryBandwidth.unit.prefix;
memBwRateUnitBase[subCluster.name] = subCluster.memoryBandwidth.unit.base;
}
}
}); });
$effect(() => {
updateTopUserConfiguration(topUserSelection.key);
});
$effect(() => {
updateTopProjectConfiguration(topProjectSelection.key);
});
/* Const Functions */
const sumUp = (data, subcluster, metric) => const sumUp = (data, subcluster, metric) =>
data.reduce( data.reduce(
(sum, node) => (sum, node) =>
@@ -239,39 +292,6 @@
0, 0,
); );
let allocatedNodes = {},
flopRate = {},
flopRateUnitPrefix = {},
flopRateUnitBase = {},
memBwRate = {},
memBwRateUnitPrefix = {},
memBwRateUnitBase = {};
$: if ($initq.data && $mainQuery.data) {
let subClusters = $initq.data.clusters.find(
(c) => c.name == cluster,
).subClusters;
for (let subCluster of subClusters) {
allocatedNodes[subCluster.name] =
$mainQuery.data.allocatedNodes.find(
({ name }) => name == subCluster.name,
)?.count || 0;
flopRate[subCluster.name] =
Math.floor(
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "flops_any") *
100,
) / 100;
flopRateUnitPrefix[subCluster.name] = subCluster.flopRateSimd.unit.prefix;
flopRateUnitBase[subCluster.name] = subCluster.flopRateSimd.unit.base;
memBwRate[subCluster.name] =
Math.floor(
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "mem_bw") * 100,
) / 100;
memBwRateUnitPrefix[subCluster.name] =
subCluster.memoryBandwidth.unit.prefix;
memBwRateUnitBase[subCluster.name] = subCluster.memoryBandwidth.unit.base;
}
}
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
client: client, client: client,
@@ -284,20 +304,17 @@
}); });
}; };
/* Functions */
function updateTopUserConfiguration(select) { function updateTopUserConfiguration(select) {
if (ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select) { if (ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select) {
updateConfigurationMutation({ updateConfigurationMutation({
name: `status_view_selectedTopUserCategory:${cluster}`, name: `status_view_selectedTopUserCategory:${cluster}`,
value: JSON.stringify(select), value: JSON.stringify(select),
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && !res.error) { if (res.fetching === false && res.error) {
// console.log(`status_view_selectedTopUserCategory:${cluster}` + ' -> Updated!')
} else if (res.fetching === false && res.error) {
throw res.error; throw res.error;
} }
}); });
} else {
// console.log('No Mutation Required: Top User')
} }
} }
@@ -309,19 +326,12 @@
name: `status_view_selectedTopProjectCategory:${cluster}`, name: `status_view_selectedTopProjectCategory:${cluster}`,
value: JSON.stringify(select), value: JSON.stringify(select),
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && !res.error) { if (res.fetching === false && res.error) {
// console.log(`status_view_selectedTopProjectCategory:${cluster}` + ' -> Updated!')
} else if (res.fetching === false && res.error) {
throw res.error; throw res.error;
} }
}); });
} else {
// console.log('No Mutation Required: Top Project')
} }
} }
$: updateTopUserConfiguration(topUserSelection.key);
$: updateTopProjectConfiguration(topProjectSelection.key);
</script> </script>
<!-- Loading indicator & Refresh --> <!-- Loading indicator & Refresh -->
@@ -334,7 +344,7 @@
<Button <Button
outline outline
color="secondary" color="secondary"
on:click={() => (isHistogramSelectionOpen = true)} onclick={() => (isHistogramSelectionOpen = true)}
> >
<Icon name="bar-chart-line" /> Select Histograms <Icon name="bar-chart-line" /> Select Histograms
</Button> </Button>
@@ -342,7 +352,7 @@
<Col class="mt-2 mt-md-0"> <Col class="mt-2 mt-md-0">
<Refresher <Refresher
initially={120} initially={120}
on:refresh={() => { onRefresh={() => {
from = new Date(Date.now() - 5 * 60 * 1000); from = new Date(Date.now() - 5 * 60 * 1000);
to = new Date(Date.now()); to = new Date(Date.now());
}} }}
@@ -483,6 +493,7 @@
<Card body color="danger">{$topUserQuery.error.message}</Card> <Card body color="danger">{$topUserQuery.error.message}</Card>
{:else} {:else}
<Pie <Pie
canvasId="hpcpie-users"
size={colWidth} size={colWidth}
sliceLabel={topUserSelection.label} sliceLabel={topUserSelection.label}
quantities={$topUserQuery.data.topUser.map( quantities={$topUserQuery.data.topUser.map(
@@ -550,6 +561,7 @@
<Card body color="danger">{$topProjectQuery.error.message}</Card> <Card body color="danger">{$topProjectQuery.error.message}</Card>
{:else} {:else}
<Pie <Pie
canvasId="hpcpie-projects"
size={colWidth} size={colWidth}
sliceLabel={topProjectSelection.label} sliceLabel={topProjectSelection.label}
quantities={$topProjectQuery.data.topProjects.map( quantities={$topProjectQuery.data.topProjects.map(
@@ -685,7 +697,10 @@
{/if} {/if}
<HistogramSelection <HistogramSelection
bind:cluster {cluster}
bind:selectedHistograms
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms}
applyChange={(newSelection) => {
selectedHistograms = [...newSelection];
}}
/> />
+95 -81
View File
@@ -28,72 +28,74 @@
import TimeSelection from "./generic/select/TimeSelection.svelte"; import TimeSelection from "./generic/select/TimeSelection.svelte";
import Refresher from "./generic/helper/Refresher.svelte"; import Refresher from "./generic/helper/Refresher.svelte";
export let displayType;
export let cluster = null;
export let subCluster = null;
export let from = null;
export let to = null;
const { query: initq } = init();
console.assert(
displayType == "OVERVIEW" || displayType == "LIST",
"Invalid nodes displayType provided!",
);
if (from == null || to == null) {
to = new Date(Date.now());
from = new Date(to.getTime());
from.setHours(from.getHours() - 12);
}
const initialized = getContext("initialized");
const ccconfig = getContext("cc-config");
const globalMetrics = getContext("globalMetrics");
const displayNodeOverview = (displayType === 'OVERVIEW')
const resampleConfig = getContext("resampling") || null;
const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
let selectedResolution = resampleConfig ? resampleDefault : 0;
let hostnameFilter = "";
let pendingHostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric || "";
let selectedMetrics = (
ccconfig[`node_list_selectedMetrics:${cluster}:${subCluster}`] ||
ccconfig[`node_list_selectedMetrics:${cluster}`]
) || [ccconfig.system_view_selectedMetric];
let isMetricsSelectionOpen = false;
/* /*
Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment) Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
*/ */
let systemMetrics = []; /* Scelte 5 Props */
let systemUnits = {}; let {
displayType,
cluster = null,
subCluster = null,
fromPreset = null,
toPreset = null,
} = $props();
function loadMetrics(isInitialized) { /* Const Init */
if (!isInitialized) return const { query: initq } = init();
systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] const displayNodeOverview = (displayType === 'OVERVIEW');
for (let sm of systemMetrics) { const ccconfig = getContext("cc-config");
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "") const initialized = getContext("initialized");
} const globalMetrics = getContext("globalMetrics");
if (!selectedMetric) selectedMetric = systemMetrics[0].name const resampleConfig = getContext("resampling") || null;
}
$: loadMetrics($initialized) const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
const nowDate = new Date(Date.now());
$: if (displayNodeOverview) { /* State Init */
selectedMetrics = [selectedMetric] let to = $state(toPreset || new Date(Date.now()));
} let from = $state(fromPreset || new Date(nowDate.setHours(nowDate.getHours() - 4)));
let selectedResolution = $state(resampleConfig ? resampleDefault : 0);
let hostnameFilter = $state("");
let pendingHostnameFilter = $state("");
let isMetricsSelectionOpen = $state(false);
let selectedMetric = $state(ccconfig.system_view_selectedMetric || "");
let selectedMetrics = $state((
ccconfig[`node_list_selectedMetrics:${cluster}:${subCluster}`] ||
ccconfig[`node_list_selectedMetrics:${cluster}`]
) || [ccconfig.system_view_selectedMetric]);
$: { // Wait after input for some time to prevent too many requests /* Derived States */
setTimeout(function () { const systemMetrics = $derived($initialized ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
const presetSystemUnits = $derived(loadUnits(systemMetrics));
/* Effects */
$effect(() => {
// OnMount: Ping Var, without this, OVERVIEW metric select is empty (reason tbd)
systemMetrics
});
/* Functions */
function loadUnits(systemMetrics) {
let pendingUnits = {};
if (systemMetrics.length > 0) {
for (let sm of systemMetrics) {
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
};
};
return {...pendingUnits};
};
// Wait after input for some time to prevent too many requests
let timeoutId = null;
function updateHostnameFilter() {
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(function () {
hostnameFilter = pendingHostnameFilter; hostnameFilter = pendingHostnameFilter;
}, 500); }, 500);
} };
</script> </script>
<!-- ROW1: Tools--> <!-- ROW1: Tools-->
@@ -108,7 +110,7 @@
<Button <Button
outline outline
color="primary" color="primary"
on:click={() => (isMetricsSelectionOpen = true)} onclick={() => (isMetricsSelectionOpen = true)}
> >
{selectedMetrics.length} selected {selectedMetrics.length} selected
</Button> </Button>
@@ -139,12 +141,20 @@
placeholder="Filter hostname ..." placeholder="Filter hostname ..."
type="text" type="text"
bind:value={pendingHostnameFilter} bind:value={pendingHostnameFilter}
oninput={updateHostnameFilter}
/> />
</InputGroup> </InputGroup>
</Col> </Col>
<!-- Range Col--> <!-- Range Col-->
<Col> <Col>
<TimeSelection bind:from bind:to /> <TimeSelection
presetFrom={from}
presetTo={to}
applyTime={(newFrom, newTo) => {
from = newFrom;
to = newTo;
}}
/>
</Col> </Col>
<!-- Overview Metric Col--> <!-- Overview Metric Col-->
{#if displayNodeOverview} {#if displayNodeOverview}
@@ -153,26 +163,28 @@
<InputGroupText><Icon name="graph-up" /></InputGroupText> <InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText> <InputGroupText>Metric</InputGroupText>
<Input type="select" bind:value={selectedMetric}> <Input type="select" bind:value={selectedMetric}>
{#each systemMetrics as metric} {#each systemMetrics as metric (metric.name)}
<option value={metric.name} <option value={metric.name}
>{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option >{metric.name} {presetSystemUnits[metric.name] ? "("+presetSystemUnits[metric.name]+")" : ""}</option
> >
{:else}
<option disabled>No available options</option>
{/each} {/each}
</Input> </Input>
</InputGroup> </InputGroup>
</Col> </Col>
{/if} {/if}
<!-- Refresh Col-->
<Col class="mt-2 mt-lg-0">
<Refresher
on:refresh={() => {
const diff = Date.now() - to;
from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff);
}}
/>
</Col>
{/if} {/if}
<!-- Refresh Col-->
<Col class="mt-2 mt-lg-0">
<Refresher
onRefresh={() => {
const diff = Date.now() - to;
from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff);
}}
/>
</Col>
</Row> </Row>
<!-- ROW2: Content--> <!-- ROW2: Content-->
@@ -185,20 +197,22 @@
{:else} {:else}
{#if displayNodeOverview} {#if displayNodeOverview}
<!-- ROW2-1: Node Overview (Grid Included)--> <!-- ROW2-1: Node Overview (Grid Included)-->
<NodeOverview {cluster} {subCluster} {ccconfig} {selectedMetrics} {from} {to} {hostnameFilter}/> <NodeOverview {cluster} {ccconfig} {selectedMetric} {from} {to} {hostnameFilter}/>
{:else} {:else}
<!-- ROW2-2: Node List (Grid Included)--> <!-- ROW2-2: Node List (Grid Included)-->
<NodeList {cluster} {subCluster} {ccconfig} {selectedMetrics} {selectedResolution} {hostnameFilter} {from} {to} {systemUnits}/> <NodeList {cluster} {subCluster} {ccconfig} {selectedMetrics} {selectedResolution} {hostnameFilter} {from} {to} {presetSystemUnits}/>
{/if} {/if}
{/if} {/if}
<MetricSelection {#if !displayNodeOverview}
{cluster} <MetricSelection
{subCluster} bind:isOpen={isMetricsSelectionOpen}
configName="node_list_selectedMetrics" presetMetrics={selectedMetrics}
metrics={selectedMetrics} {cluster}
bind:isOpen={isMetricsSelectionOpen} {subCluster}
on:update-metrics={({ detail }) => { configName="node_list_selectedMetrics"
selectedMetrics = [...detail] applyMetrics={(newMetrics) =>
}} selectedMetrics = [...newMetrics]
/> }
/>
{/if}
+12 -5
View File
@@ -24,15 +24,22 @@
init, init,
} from "./generic/utils.js"; } from "./generic/utils.js";
export let username; /* Svelte 5 Props */
export let isAdmin; let {
export let tagmap; username,
isAdmin,
presetTagmap,
} = $props();
/* Const Init */
const {} = init(); const {} = init();
const client = getContextClient(); const client = getContextClient();
let pendingChange = "none"; /* State Init */
let pendingChange = $state("none");
let tagmap = $state(presetTagmap)
/* Functions */
const removeTagMutation = ({ tagIds }) => { const removeTagMutation = ({ tagIds }) => {
return mutationStore({ return mutationStore({
client: client, client: client,
@@ -96,7 +103,7 @@
<Button <Button
size="sm" size="sm"
color="danger" color="danger"
on:click={() => removeTag(tag, tagType)} onclick={() => removeTag(tag, tagType)}
> >
<Icon name="x" /> <Icon name="x" />
</Button> </Button>
+115 -73
View File
@@ -42,72 +42,102 @@
import TextFilter from "./generic/helper/TextFilter.svelte" import TextFilter from "./generic/helper/TextFilter.svelte"
import Refresher from "./generic/helper/Refresher.svelte"; import Refresher from "./generic/helper/Refresher.svelte";
/* Svelte 5 Props */
let { user, filterPresets } = $props();
/* Const Init */
const { query: initq } = init(); const { query: initq } = init();
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
export let user;
export let filterPresets;
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobList;
let jobFilters = [];
let matchedListJobs = 0;
let sorting = { field: "startTime", type: "col", order: "DESC" },
isSortingOpen = false;
let metrics = ccconfig.plot_list_selectedMetrics,
isMetricsSelectionOpen = false;
let isHistogramSelectionOpen = false;
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
let showFootprint = filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint;
let numDurationBins = "1h";
let numMetricBins = 10;
let durationBinOptions = ["1m","10m","1h","6h","12h"];
let metricBinOptions = [10, 20, 50, 100];
$: selectedHistograms = selectedCluster
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig['user_view_histogramMetrics'] || [];
const client = getContextClient(); const client = getContextClient();
$: stats = queryStore({ const durationBinOptions = ["1m","10m","1h","6h","12h"];
client: client, const metricBinOptions = [10, 20, 50, 100];
query: gql`
query ($jobFilters: [JobFilter!]!, $selectedHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) { /* State Init */
jobsStatistics(filter: $jobFilters, metrics: $selectedHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) { // List & Control Vars
totalJobs 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
shortJobs let jobFilters = $state([]);
totalWalltime let jobList = $state(null);
totalCoreHours let matchedListJobs = $state(0);
histDuration { let isSortingOpen = $state(false);
count let isMetricsSelectionOpen = $state(false);
value let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
} let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
histNumNodes { let selectedHistogramsBuffer = $state({ all: (ccconfig['user_view_histogramMetrics'] || []) })
count let metrics = $state(filterPresets.cluster
value ? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
} ccconfig.plot_list_selectedMetrics
histMetrics { : ccconfig.plot_list_selectedMetrics
metric );
unit let showFootprint = $state(filterPresets.cluster
stat ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
data { : !!ccconfig.plot_list_showFootprint
min );
max
// Histogram Vars
let isHistogramSelectionOpen = $state(false);
let numDurationBins = $state("1h");
let numMetricBins = $state(10);
// Compare Vars (TODO)
// let jobCompare = $state(null);
// let showCompare = $state(false);
// let selectedJobs = $state([]);
// let filterBuffer = $state([]);
// let matchedCompareJobs = $state(0);
/* Derived Vars */
let selectedHistograms = $derived(selectedCluster ? selectedHistogramsBuffer[selectedCluster] : selectedHistogramsBuffer['all']);
let stats = $derived(
queryStore({
client: client,
query: gql`
query ($jobFilters: [JobFilter!]!, $selectedHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) {
jobsStatistics(filter: $jobFilters, metrics: $selectedHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) {
totalJobs
shortJobs
totalWalltime
totalCoreHours
histDuration {
count count
bin value
}
histNumNodes {
count
value
}
histMetrics {
metric
unit
stat
data {
min
max
count
bin
}
} }
} }
} }
} `,
`, variables: { jobFilters, selectedHistograms, numDurationBins, numMetricBins },
variables: { jobFilters, selectedHistograms, numDurationBins, numMetricBins }, })
);
/* Effect */
$effect(() => {
if (!selectedHistogramsBuffer[selectedCluster]) {
selectedHistogramsBuffer[selectedCluster] = ccconfig[`user_view_histogramMetrics:${selectedCluster}`];
};
}); });
onMount(() => filterComponent.updateFilters()); /* On Mount */
onMount(() => {
filterComponent.updateFilters();
// Why? -> `$derived(ccconfig[$cluster])` only loads array from last Backend-Query if $cluster changed reactively (without reload)
if (filterPresets?.cluster) {
selectedHistogramsBuffer[filterPresets.cluster] = ccconfig[`user_view_histogramMetrics:${filterPresets.cluster}`];
};
});
</script> </script>
<!-- ROW1: Status--> <!-- ROW1: Status-->
@@ -129,13 +159,13 @@
<Row cols={{ xs: 1, md: 2, lg: 6}} class="mb-3"> <Row cols={{ xs: 1, md: 2, lg: 6}} class="mb-3">
<Col class="mb-2 mb-lg-0"> <Col class="mb-2 mb-lg-0">
<ButtonGroup class="w-100"> <ButtonGroup class="w-100">
<Button outline color="primary" on:click={() => (isSortingOpen = true)}> <Button outline color="primary" onclick={() => (isSortingOpen = true)}>
<Icon name="sort-up" /> Sorting <Icon name="sort-up" /> Sorting
</Button> </Button>
<Button <Button
outline outline
color="primary" color="primary"
on:click={() => (isMetricsSelectionOpen = true)} onclick={() => (isMetricsSelectionOpen = true)}
> >
<Icon name="graph-up" /> Metrics <Icon name="graph-up" /> Metrics
</Button> </Button>
@@ -143,11 +173,11 @@
</Col> </Col>
<Col lg="4" class="mb-1 mb-lg-0"> <Col lg="4" class="mb-1 mb-lg-0">
<Filters <Filters
bind:this={filterComponent}
{filterPresets} {filterPresets}
matchedJobs={matchedListJobs} matchedJobs={matchedListJobs}
startTimeQuickSelect={true} startTimeQuickSelect={true}
bind:this={filterComponent} applyFilters={(detail) => {
on:update-filters={({ detail }) => {
jobFilters = [...detail.filters, { user: { eq: user.username } }]; jobFilters = [...detail.filters, { user: { eq: user.username } }];
selectedCluster = jobFilters[0]?.cluster selectedCluster = jobFilters[0]?.cluster
? jobFilters[0].cluster.eq ? jobFilters[0].cluster.eq
@@ -173,11 +203,11 @@
</Col> </Col>
<Col class="mb-2 mb-lg-0"> <Col class="mb-2 mb-lg-0">
<TextFilter <TextFilter
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)} setFilter={(filter) => filterComponent.updateFilters(filter)}
/> />
</Col> </Col>
<Col class="mb-1 mb-lg-0"> <Col class="mb-1 mb-lg-0">
<Refresher on:refresh={() => { <Refresher onRefresh={() => {
jobList.refreshJobs() jobList.refreshJobs()
jobList.refreshAllMetrics() jobList.refreshAllMetrics()
}} /> }} />
@@ -269,7 +299,7 @@
outline outline
color="secondary" color="secondary"
class="w-100" class="w-100"
on:click={() => (isHistogramSelectionOpen = true)} onclick={() => (isHistogramSelectionOpen = true)}
> >
<Icon name="bar-chart-line" /> Select Histograms <Icon name="bar-chart-line" /> Select Histograms
</Button> </Button>
@@ -344,19 +374,31 @@
</Col> </Col>
</Row> </Row>
<Sorting bind:sorting bind:isOpen={isSortingOpen} /> <Sorting
bind:isOpen={isSortingOpen}
presetSorting={sorting}
applySorting={(newSort) =>
sorting = {...newSort}
}
/>
<MetricSelection <MetricSelection
bind:cluster={selectedCluster} bind:isOpen={isMetricsSelectionOpen}
configName="plot_list_selectedMetrics" bind:showFootprint
bind:metrics presetMetrics={metrics}
bind:isOpen={isMetricsSelectionOpen} cluster={selectedCluster}
bind:showFootprint configName="plot_list_selectedMetrics"
footprintSelect applyMetrics={(newMetrics) =>
metrics = [...newMetrics]
}
footprintSelect
/> />
<HistogramSelection <HistogramSelection
bind:cluster={selectedCluster} cluster={selectedCluster}
bind:selectedHistograms
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms}
applyChange={(newSelection) => {
selectedHistogramsBuffer[selectedCluster || 'all'] = [...newSelection];
}}
/> />
+2 -1
View File
@@ -1,9 +1,10 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import Analysis from './Analysis.root.svelte' import Analysis from './Analysis.root.svelte'
filterPresets.cluster = cluster filterPresets.cluster = cluster
new Analysis({ mount(Analysis, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
filterPresets: filterPresets, filterPresets: filterPresets,
+20 -14
View File
@@ -21,10 +21,14 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let availableMetrics; /* Svelte 5 Props */
export let metricsInHistograms; let {
export let metricsInScatterplots; availableMetrics,
metricsInHistograms = $bindable(),
metricsInScatterplots = $bindable(),
} = $props();
/* Const Init */
const client = getContextClient(); const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@@ -38,11 +42,13 @@
}); });
}; };
let isHistogramConfigOpen = false, /* State Init */
isScatterPlotConfigOpen = false; let isHistogramConfigOpen = $state(false);
let selectedMetric1 = null, let isScatterPlotConfigOpen = $state(false);
selectedMetric2 = null; let selectedMetric1 = $state(null);
let selectedMetric2 = $state(null);
/* Functions */
function updateConfiguration(data) { function updateConfiguration(data) {
updateConfigurationMutation({ updateConfigurationMutation({
name: data.name, name: data.name,
@@ -55,12 +61,12 @@
} }
</script> </script>
<Button outline on:click={() => (isHistogramConfigOpen = true)}> <Button outline onclick={() => (isHistogramConfigOpen = true)}>
<Icon name="" /> <Icon name="" />
Select Plots for Histograms Select Plots for Histograms
</Button> </Button>
<Button outline on:click={() => (isScatterPlotConfigOpen = true)}> <Button outline onclick={() => (isScatterPlotConfigOpen = true)}>
<Icon name="" /> <Icon name="" />
Select Plots in Scatter Plots Select Plots in Scatter Plots
</Button> </Button>
@@ -78,7 +84,7 @@
type="checkbox" type="checkbox"
bind:group={metricsInHistograms} bind:group={metricsInHistograms}
value={metric} value={metric}
on:change={() => onchange={() =>
updateConfiguration({ updateConfiguration({
name: "analysis_view_histogramMetrics", name: "analysis_view_histogramMetrics",
value: metricsInHistograms, value: metricsInHistograms,
@@ -91,7 +97,7 @@
</ListGroup> </ListGroup>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" on:click={() => (isHistogramConfigOpen = false)}> <Button color="primary" onclick={() => (isHistogramConfigOpen = false)}>
Close Close
</Button> </Button>
</ModalFooter> </ModalFooter>
@@ -112,7 +118,7 @@
style="float: right;" style="float: right;"
outline outline
color="danger" color="danger"
on:click={() => { onclick={() => {
metricsInScatterplots = metricsInScatterplots.filter( metricsInScatterplots = metricsInScatterplots.filter(
(p) => pair != p, (p) => pair != p,
); );
@@ -146,7 +152,7 @@
<Button <Button
outline outline
disabled={selectedMetric1 == null || selectedMetric2 == null} disabled={selectedMetric1 == null || selectedMetric2 == null}
on:click={() => { onclick={() => {
metricsInScatterplots = [ metricsInScatterplots = [
...metricsInScatterplots, ...metricsInScatterplots,
[selectedMetric1, selectedMetric2], [selectedMetric1, selectedMetric2],
@@ -164,7 +170,7 @@
</InputGroup> </InputGroup>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" on:click={() => (isScatterPlotConfigOpen = false)}> <Button color="primary" onclick={() => (isScatterPlotConfigOpen = false)}>
Close Close
</Button> </Button>
</ModalFooter> </ModalFooter>
+2 -1
View File
@@ -1,7 +1,8 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import Config from './Config.root.svelte' import Config from './Config.root.svelte'
new Config({ mount(Config, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
isAdmin: isAdmin, isAdmin: isAdmin,
+13 -8
View File
@@ -12,13 +12,17 @@
import Options from "./admin/Options.svelte"; import Options from "./admin/Options.svelte";
import NoticeEdit from "./admin/NoticeEdit.svelte"; import NoticeEdit from "./admin/NoticeEdit.svelte";
export let ncontent; /* Svelte 5 Props */
let { ncontent } = $props();
let users = [];
let roles = [];
/* Const Init*/
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
/* State Init */
let users = $state([]);
let roles = $state([]);
/* Functions */
function getUserList() { function getUserList() {
fetch("/config/users/?via-ldap=false&not-just-user=true") fetch("/config/users/?via-ldap=false&not-just-user=true")
.then((res) => res.json()) .then((res) => res.json())
@@ -40,21 +44,22 @@
getValidRoles(); getValidRoles();
} }
/* on Mount */
onMount(() => initAdmin()); onMount(() => initAdmin());
</script> </script>
<Row cols={2} class="p-2 g-2"> <Row cols={2} class="p-2 g-2">
<Col class="mb-1"> <Col class="mb-1">
<AddUser {roles} on:reload={getUserList} /> <AddUser {roles} reloadUser={() => getUserList()} />
</Col> </Col>
<Col class="mb-1"> <Col class="mb-1">
<ShowUsers on:reload={getUserList} bind:users /> <ShowUsers reloadUser={() => getUserList()} bind:users />
</Col> </Col>
<Col> <Col>
<EditRole {roles} on:reload={getUserList} /> <EditRole {roles} reloadUser={() => getUserList()} />
</Col> </Col>
<Col> <Col>
<EditProject on:reload={getUserList} /> <EditProject reloadUser={() => getUserList()} />
</Col> </Col>
<Options config={ccconfig}/> <Options config={ccconfig}/>
<NoticeEdit {ncontent}/> <NoticeEdit {ncontent}/>
+20 -11
View File
@@ -12,17 +12,26 @@
import PlotRenderOptions from "./user/PlotRenderOptions.svelte"; import PlotRenderOptions from "./user/PlotRenderOptions.svelte";
import PlotColorScheme from "./user/PlotColorScheme.svelte"; import PlotColorScheme from "./user/PlotColorScheme.svelte";
export let username /* Svelte 5 Props */
export let isApi let {
username,
isApi
} = $props();
/* Const Init */
const ccconfig = getContext("cc-config"); const ccconfig = getContext("cc-config");
let message = { msg: "", target: "", color: "#d63384" };
let displayMessage = false; /* State Init */
let cbmode = ccconfig?.plot_general_colorblindMode || false; let message = $state({ msg: "", target: "", color: "#d63384" });
let displayMessage = $state(false);
let cbmode = $state(ccconfig?.plot_general_colorblindMode || false);
async function handleSettingSubmit(event) { /* Functions */
const selector = event.detail.selector async function handleSettingSubmit(event, setting) {
const target = event.detail.target event.preventDefault();
const selector = setting.selector
const target = setting.target
let form = document.querySelector(selector); let form = document.querySelector(selector);
let formData = new FormData(form); let formData = new FormData(form);
try { try {
@@ -53,6 +62,6 @@
} }
</script> </script>
<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/> <UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/> <PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
<PlotColorScheme config={ccconfig} bind:cbmode bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/> <PlotColorScheme config={ccconfig} bind:cbmode bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
+23 -20
View File
@@ -10,17 +10,19 @@
<script> <script>
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap"; import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let { roles, reloadUser } = $props();
let message = { msg: "", color: "#d63384" }; /* State Init */
let displayMessage = false; let message = $state({ msg: "", color: "#d63384" });
let displayMessage = $state(false);
export let roles; /* Functions */
async function handleUserSubmit(event) {
async function handleUserSubmit() { event.preventDefault();
let form = document.querySelector("#create-user-form"); let form = document.querySelector("#create-user-form");
let formData = new FormData(form); let formData = new FormData(form);
@@ -29,7 +31,7 @@
if (res.ok) { if (res.ok) {
let text = await res.text(); let text = await res.text();
popMessage(text, "#048109"); popMessage(text, "#048109");
reloadUserList(); reloadUser();
form.reset(); form.reset();
} else { } else {
let text = await res.text(); let text = await res.text();
@@ -47,10 +49,6 @@
displayMessage = false; displayMessage = false;
}, 3500); }, 3500);
} }
function reloadUserList() {
dispatch("reload");
}
</script> </script>
<Card> <Card>
@@ -59,7 +57,8 @@
method="post" method="post"
action="/config/users/" action="/config/users/"
class="card-body" class="card-body"
on:submit|preventDefault={handleUserSubmit} autocomplete="off"
onsubmit={(e) => handleUserSubmit(e)}
> >
<CardTitle class="mb-3">Create User</CardTitle> <CardTitle class="mb-3">Create User</CardTitle>
<div class="mb-3"> <div class="mb-3">
@@ -70,6 +69,7 @@
id="username" id="username"
name="username" name="username"
aria-describedby="usernameHelp" aria-describedby="usernameHelp"
autocomplete="username"
/> />
<div id="usernameHelp" class="form-text">Must be unique.</div> <div id="usernameHelp" class="form-text">Must be unique.</div>
</div> </div>
@@ -81,6 +81,7 @@
id="password" id="password"
name="password" name="password"
aria-describedby="passwordHelp" aria-describedby="passwordHelp"
autocomplete="new-password"
/> />
<div id="passwordHelp" class="form-text"> <div id="passwordHelp" class="form-text">
Only API users are allowed to have a blank password. Users with a blank Only API users are allowed to have a blank password. Users with a blank
@@ -109,6 +110,7 @@
id="name" id="name"
name="name" name="name"
aria-describedby="nameHelp" aria-describedby="nameHelp"
autocomplete="name"
/> />
<div id="nameHelp" class="form-text">Optional, can be blank.</div> <div id="nameHelp" class="form-text">Optional, can be blank.</div>
</div> </div>
@@ -120,6 +122,7 @@
id="email" id="email"
name="email" name="email"
aria-describedby="emailHelp" aria-describedby="emailHelp"
autocomplete="email"
/> />
<div id="emailHelp" class="form-text">Optional, can be blank.</div> <div id="emailHelp" class="form-text">Optional, can be blank.</div>
</div> </div>
@@ -153,13 +156,13 @@
{/each} {/each}
</div> </div>
<p style="display: flex; align-items: center;"> <p style="display: flex; align-items: center;">
<Button type="submit" color="primary">Submit</Button> <Button type="submit" color="primary" style="margin-right: 1.5em;">Submit</Button>
{#if displayMessage}<div style="margin-left: 1.5em;"> {#if displayMessage}
<b <b
><code style="color: {message.color};" out:fade>{message.msg}</code ><code style="color: {message.color};" out:fade>{message.msg}</code
></b ></b
> >
</div>{/if} {/if}
</p> </p>
</form> </form>
</Card> </Card>
@@ -7,15 +7,19 @@
<script> <script>
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap"; import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let { reloadUser } = $props();
let message = { msg: "", color: "#d63384" }; /* State Init */
let displayMessage = false; let message = $state({ msg: "", color: "#d63384" });
let displayMessage = $state(false);
/* Functions */
async function handleAddProject(event) {
event.preventDefault();
async function handleAddProject() {
const username = document.querySelector("#project-username").value; const username = document.querySelector("#project-username").value;
const project = document.querySelector("#project-id").value; const project = document.querySelector("#project-id").value;
@@ -36,7 +40,7 @@
if (res.ok) { if (res.ok) {
let text = await res.text(); let text = await res.text();
popMessage(text, "#048109"); popMessage(text, "#048109");
reloadUserList(); reloadUser();
} else { } else {
let text = await res.text(); let text = await res.text();
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
@@ -46,7 +50,9 @@
} }
} }
async function handleRemoveProject() { async function handleRemoveProject(event) {
event.preventDefault();
const username = document.querySelector("#project-username").value; const username = document.querySelector("#project-username").value;
const project = document.querySelector("#project-id").value; const project = document.querySelector("#project-id").value;
@@ -67,7 +73,7 @@
if (res.ok) { if (res.ok) {
let text = await res.text(); let text = await res.text();
popMessage(text, "#048109"); popMessage(text, "#048109");
reloadUserList(); reloadUser();
} else { } else {
let text = await res.text(); let text = await res.text();
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
@@ -84,10 +90,6 @@
displayMessage = false; displayMessage = false;
}, 3500); }, 3500);
} }
function reloadUserList() {
dispatch("reload");
}
</script> </script>
<Card> <Card>
@@ -108,19 +110,17 @@
placeholder="project-id" placeholder="project-id"
id="project-id" id="project-id"
/> />
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button <button
class="btn btn-primary" class="btn btn-primary"
type="button" type="button"
id="add-project-button" id="add-project-button"
on:click|preventDefault={handleAddProject}>Add</button onclick={(e) => handleAddProject(e)}>Add</button
> >
<button <button
class="btn btn-danger" class="btn btn-danger"
type="button" type="button"
id="remove-project-button" id="remove-project-button"
on:click|preventDefault={handleRemoveProject}>Remove</button onclick={(e) => handleRemoveProject(e)}>Remove</button
> >
</div> </div>
<p> <p>
+15 -17
View File
@@ -10,17 +10,19 @@
<script> <script>
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap"; import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
const dispatch = createEventDispatcher(); /* SVelte 5 Props */
let {roles, reloadUser } = $props();
let message = { msg: "", color: "#d63384" }; /* State Init */
let displayMessage = false; let message = $state({ msg: "", color: "#d63384" });
let displayMessage = $state(false);
export let roles; /* Functions */
async function handleAddRole(event) {
event.preventDefault();
async function handleAddRole() {
const username = document.querySelector("#role-username").value; const username = document.querySelector("#role-username").value;
const role = document.querySelector("#role-select").value; const role = document.querySelector("#role-select").value;
@@ -41,7 +43,7 @@
if (res.ok) { if (res.ok) {
let text = await res.text(); let text = await res.text();
popMessage(text, "#048109"); popMessage(text, "#048109");
reloadUserList(); reloadUser();
} else { } else {
let text = await res.text(); let text = await res.text();
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
@@ -51,7 +53,9 @@
} }
} }
async function handleRemoveRole() { async function handleRemoveRole(event) {
event.preventDefault();
const username = document.querySelector("#role-username").value; const username = document.querySelector("#role-username").value;
const role = document.querySelector("#role-select").value; const role = document.querySelector("#role-select").value;
@@ -72,7 +76,7 @@
if (res.ok) { if (res.ok) {
let text = await res.text(); let text = await res.text();
popMessage(text, "#048109"); popMessage(text, "#048109");
reloadUserList(); reloadUser();
} else { } else {
let text = await res.text(); let text = await res.text();
throw new Error("Response Code " + res.status + "-> " + text); throw new Error("Response Code " + res.status + "-> " + text);
@@ -89,10 +93,6 @@
displayMessage = false; displayMessage = false;
}, 3500); }, 3500);
} }
function reloadUserList() {
dispatch("reload");
}
</script> </script>
<Card> <Card>
@@ -113,19 +113,17 @@
> >
{/each} {/each}
</select> </select>
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button <button
class="btn btn-primary" class="btn btn-primary"
type="button" type="button"
id="add-role-button" id="add-role-button"
on:click|preventDefault={handleAddRole}>Add</button onclick={(e) => handleAddRole(e)}>Add</button
> >
<button <button
class="btn btn-danger" class="btn btn-danger"
type="button" type="button"
id="remove-role-button" id="remove-role-button"
on:click|preventDefault={handleRemoveRole}>Remove</button onclick={(e) =>handleRemoveRole(e)}>Remove</button
> >
</div> </div>
<p> <p>
@@ -6,14 +6,18 @@
import { Col, Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap"; import { Col, Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
export let ncontent; /* Svelte 5 Props */
let { ncontent } = $props();
let message = { msg: "", color: "#d63384" }; /* State Init */
let displayMessage = false; let message = $state({ msg: "", color: "#d63384" });
let displayMessage = $state(false);
/* Functions */
async function handleEditNotice(event) {
event.preventDefault();
async function handleEditNotice() {
const content = document.querySelector("#notice-content").value; const content = document.querySelector("#notice-content").value;
let formData = new FormData(); let formData = new FormData();
formData.append("new-content", content); formData.append("new-content", content);
@@ -56,14 +60,11 @@
value={ncontent} value={ncontent}
id="notice-content" id="notice-content"
/> />
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button <button
class="btn btn-primary" class="btn btn-primary"
type="button" type="button"
id="edit-notice-button" id="edit-notice-button"
on:click|preventDefault={handleEditNotice}>Edit Notice</button onclick={(e) => handleEditNotice(e)}>Edit Notice</button
> >
</div> </div>
<p> <p>
+6 -3
View File
@@ -6,10 +6,13 @@
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { Col, Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap"; import { Col, Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
let scrambled; /*Const Init */
const resampleConfig = getContext("resampling"); const resampleConfig = getContext("resampling");
/* State Init */
let scrambled = $state(false);
/* on Mount */
onMount(() => { onMount(() => {
scrambled = window.localStorage.getItem("cc-scramble-names") != null; scrambled = window.localStorage.getItem("cc-scramble-names") != null;
}); });
@@ -33,7 +36,7 @@
type="checkbox" type="checkbox"
id="scramble-names-checkbox" id="scramble-names-checkbox"
style="margin-right: 1em;" style="margin-right: 1em;"
on:click={handleScramble} onclick={() => handleScramble()}
bind:checked={scrambled} bind:checked={scrambled}
/> />
Active? Active?
+8 -13
View File
@@ -16,23 +16,19 @@
CardTitle, CardTitle,
CardBody, CardBody,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import ShowUsersRow from "./ShowUsersRow.svelte"; import ShowUsersRow from "./ShowUsersRow.svelte";
export let users = []; /*Svelte 5 Props */
let { users = $bindable([]), reloadUser } = $props();
const dispatch = createEventDispatcher();
function reloadUserList() {
dispatch("reload");
}
/* Functions */
function deleteUser(username) { function deleteUser(username) {
if (confirm("Are you sure?")) { if (confirm("Are you sure?")) {
let formData = new FormData(); let formData = new FormData();
formData.append("username", username); formData.append("username", username);
fetch("/config/users/", { method: "DELETE", body: formData }).then((res) => { fetch("/config/users/", { method: "DELETE", body: formData }).then((res) => {
if (res.status == 200) { if (res.status == 200) {
reloadUserList(); reloadUser();
} else { } else {
confirm(res.statusText); confirm(res.statusText);
} }
@@ -40,7 +36,6 @@
} }
} }
$: userList = users;
</script> </script>
<Card class="h-100"> <Card class="h-100">
@@ -53,11 +48,11 @@
<Button <Button
color="secondary" color="secondary"
size="sm" size="sm"
on:click={reloadUserList} onclick={() => reloadUser()}
style="float: right;">Reload</Button style="float: right;">Reload</Button
> >
</p> </p>
<div style="width: 100%; max-height: 500px; overflow-y: scroll;"> <div style="width: 100%; max-height: 725px; overflow-y: scroll;">
<Table hover> <Table hover>
<thead> <thead>
<tr> <tr>
@@ -71,13 +66,13 @@
</tr> </tr>
</thead> </thead>
<tbody id="users-list"> <tbody id="users-list">
{#each userList as user} {#each users as user}
<tr id="user-{user.username}"> <tr id="user-{user.username}">
<ShowUsersRow {user} /> <ShowUsersRow {user} />
<td <td
><button ><button
class="btn btn-danger del-user" class="btn btn-danger del-user"
on:click={deleteUser(user.username)}>Delete</button onclick={() => deleteUser(user.username)}>Delete</button
></td ></td
> >
</tr> </tr>
@@ -10,9 +10,13 @@
import { Button } from "@sveltestrap/sveltestrap"; import { Button } from "@sveltestrap/sveltestrap";
import { fetchJwt } from "../../generic/utils.js" import { fetchJwt } from "../../generic/utils.js"
export let user; /* Svelte 5 Props */
let { user } = $props();
let jwt = ""; /* State Init */
let jwt = $state("");
/* Functions */
function getUserJwt(username) { function getUserJwt(username) {
const p = fetchJwt(username); const p = fetchJwt(username);
p.then((content) => { p.then((content) => {
@@ -30,7 +34,7 @@
<td><code>{user?.roles ? user.roles.join(", ") : "No Roles"}</code></td> <td><code>{user?.roles ? user.roles.join(", ") : "No Roles"}</code></td>
<td> <td>
{#if !jwt} {#if !jwt}
<Button color="success" on:click={getUserJwt(user.username)} <Button color="success" onclick={() => getUserJwt(user.username)}
>Gen. JWT</Button >Gen. JWT</Button
> >
{:else} {:else}
@@ -3,15 +3,20 @@
--> -->
<script> <script>
import { Row, Col, Card, CardTitle, Button} from "@sveltestrap/sveltestrap"; import { Row, Col, Card, CardTitle, CardBody, Button} from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
export let config; /* Svelte 5 Props */
let { config } = $props();
let message; /* State Init */
let displayMessage; let message = $state("");
let displayMessage = $state(false);
/* Functions */
async function handleSettingSubmit(event, selector, target) {
event.preventDefault()
async function handleSettingSubmit(selector, target) {
let form = document.querySelector(selector); let form = document.querySelector(selector);
let formData = new FormData(form); let formData = new FormData(form);
try { try {
@@ -42,48 +47,45 @@
<Row cols={1} class="p-2 g-2"> <Row cols={1} class="p-2 g-2">
<Col> <Col>
<Card class="h-100"> <Card class="h-100">
<form <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
id="node-paging-form" <CardTitle class="m-3 d-flex align-items-center">
method="post" <div>Node List Paging Type</div>
action="/frontend/configuration/" {#if displayMessage && message.target == "npag"}<div
class="card-body" style="margin-left: auto; font-size: 0.9em;"
on:submit|preventDefault={() => >
handleSettingSubmit("#node-paging-form", "npag")} <code style="color: {message.color};" out:fade
> >Update: {message.msg}</code
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle
style="margin-bottom: 1em; display: flex; align-items: center;"
>
<div>Node List Paging Type</div>
{#if displayMessage && message.target == "npag"}<div
style="margin-left: auto; font-size: 0.9em;"
> >
<code style="color: {message.color};" out:fade </div>{/if}
>Update: {message.msg}</code </CardTitle>
> <CardBody>
</div>{/if} <form
</CardTitle> id="node-paging-form"
<input type="hidden" name="key" value="node_list_usePaging" /> method="post"
<div class="mb-3"> action="/frontend/configuration/"
<div> onsubmit={(e) => handleSettingSubmit(e, "#node-paging-form", "npag")}>
{#if config?.node_list_usePaging} <input type="hidden" name="key" value="node_list_usePaging" />
<input type="radio" id="nodes-true-checked" name="value" value="true" checked /> <div class="mb-3">
{:else} <div>
<input type="radio" id="nodes-true" name="value" value="true" /> {#if config?.node_list_usePaging}
{/if} <input type="radio" id="nodes-true-checked" name="value" value="true" checked />
<label for="true">Paging with selectable count of nodes.</label> {:else}
<input type="radio" id="nodes-true" name="value" value="true" />
{/if}
<label for="nodes-true">Paging with selectable count of nodes.</label>
</div>
<div>
{#if config?.node_list_usePaging}
<input type="radio" id="nodes-false" name="value" value="false" />
{:else}
<input type="radio" id="nodes-false-checked" name="value" value="false" checked />
{/if}
<label for="nodes-false">Continuous scroll iteratively adding 10 nodes.</label>
</div>
</div> </div>
<div> <Button color="primary" type="submit">Submit</Button>
{#if config?.node_list_usePaging} </form>
<input type="radio" id="nodes-false" name="value" value="false" /> </CardBody>
{:else}
<input type="radio" id="nodes-false-checked" name="value" value="false" checked />
{/if}
<label for="false">Continuous scroll iteratively adding 10 nodes.</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card> </Card>
</Col> </Col>
</Row> </Row>
@@ -19,21 +19,20 @@
CardTitle, CardTitle,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { createEventDispatcher } from 'svelte';
export let config; /* Svelte 5 Props */
export let message; let {
export let displayMessage; config,
export let cbmode = false; message = $bindable(),
displayMessage = $bindable(),
cbmode = $bindable(false),
updateSetting
} = $props();
const dispatch = createEventDispatcher(); /* State Init */
function updateSetting(selector, target) { let activeRow = $state(JSON.stringify(config?.plot_general_colorscheme));
dispatch('update-config', {
selector: selector, /* Const Init */
target: target
});
}
const colorschemes = { const colorschemes = {
Default: [ Default: [
"#00bfff", "#00bfff",
@@ -321,7 +320,6 @@
"rgb(189,189,189)", "rgb(189,189,189)",
] ]
} }
</script> </script>
<Row cols={1} class="p-2 g-2"> <Row cols={1} class="p-2 g-2">
@@ -350,37 +348,34 @@
<input type="hidden" name="key" value="plot_general_colorscheme" /> <input type="hidden" name="key" value="plot_general_colorscheme" />
<Table hover> <Table hover>
<tbody> <tbody>
{#each Object.entries(cbmode ? cvdschemes : colorschemes) as [name, rgbrow]} {#key activeRow}
<tr> {#each Object.entries(cbmode ? cvdschemes : colorschemes) as [name, rgbrow]}
<th scope="col">{name}</th> <tr>
<td> <th scope="col">{name}</th>
{#if rgbrow.join(",") == config.plot_general_colorscheme} <td>
<input <input
type="radio" type="radio"
name="value" name="value"
value={JSON.stringify(rgbrow)} value={JSON.stringify(rgbrow)}
checked checked={activeRow == JSON.stringify(rgbrow)}
on:click={() => onclick={(e) => {
updateSetting("#colorscheme-form", "cs")} activeRow = JSON.stringify(rgbrow)
updateSetting(e, {
selector: "#colorscheme-form",
target: "cs",
});
}}
/> />
{:else} </td>
<input <td>
type="radio" {#each rgbrow as rgb}
name="value" <span class="color-dot" style="background-color: {rgb};"
value={JSON.stringify(rgbrow)} ></span>
on:click={() => {/each}
updateSetting("#colorscheme-form", "cs")} </td>
/> </tr>
{/if} {/each}
</td> {/key}
<td>
{#each rgbrow as rgb}
<span class="color-dot" style="background-color: {rgb};"
></span>
{/each}
</td>
</tr>
{/each}
</tbody> </tbody>
</Table> </Table>
</form> </form>
@@ -19,33 +19,29 @@
CardTitle, CardTitle,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { createEventDispatcher } from 'svelte';
export let config; /* Svelte 5 Props */
export let message; let {
export let displayMessage; config,
message = $bindable(),
const dispatch = createEventDispatcher(); displayMessage = $bindable(),
function updateSetting(selector, target) { updateSetting
dispatch('update-config', { } = $props();
selector: selector,
target: target
});
}
</script> </script>
<Row cols={3} class="p-2 g-2"> <Row cols={3} class="p-2 g-2">
<!-- LINE WIDTH --> <!-- LINE WIDTH -->
<Col <Col
><Card class="h-100"> ><Card class="h-100">
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
<form <form
id="line-width-form" id="line-width-form"
method="post" method="post"
action="/frontend/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => onsubmit={(e) => updateSetting(e, {
updateSetting("#line-width-form", "lw")} selector: "#line-width-form",
target: "lw",
})}
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle <CardTitle
@@ -90,8 +86,10 @@
method="post" method="post"
action="/frontend/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => onsubmit={(e) => updateSetting(e, {
updateSetting("#plots-per-row-form", "ppr")} selector: "#plots-per-row-form",
target: "ppr",
})}
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle <CardTitle
@@ -136,8 +134,10 @@
method="post" method="post"
action="/frontend/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => onsubmit={(e) => updateSetting(e, {
updateSetting("#backgrounds-form", "bg")} selector: "#backgrounds-form",
target: "bg",
})}
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle <CardTitle
@@ -156,17 +156,17 @@
<div class="mb-3"> <div class="mb-3">
<div> <div>
{#if config.plot_general_colorBackground} {#if config.plot_general_colorBackground}
<input type="radio" id="true-checked" name="value" value="true" checked /> <input type="radio" id="colb-true-checked" name="value" value="true" checked />
{:else} {:else}
<input type="radio" id="true" name="value" value="true" /> <input type="radio" id="colb-true" name="value" value="true" />
{/if} {/if}
<label for="true">Yes</label> <label for="true">Yes</label>
</div> </div>
<div> <div>
{#if config.plot_general_colorBackground} {#if config.plot_general_colorBackground}
<input type="radio" id="false" name="value" value="false" /> <input type="radio" id="colb-false" name="value" value="false" />
{:else} {:else}
<input type="radio" id="false-checked" name="value" value="false" checked /> <input type="radio" id="colb-false-checked" name="value" value="false" checked />
{/if} {/if}
<label for="false">No</label> <label for="false">No</label>
</div> </div>
@@ -180,8 +180,10 @@
method="post" method="post"
action="/frontend/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => onsubmit={(e) => updateSetting(e, {
updateSetting("#colorblindmode-form", "cbm")} selector: "#colorblindmode-form",
target: "cbm",
})}
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle <CardTitle
+22 -22
View File
@@ -22,16 +22,23 @@
CardBody CardBody
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { createEventDispatcher } from 'svelte';
import { fetchJwt } from "../../generic/utils.js"; import { fetchJwt } from "../../generic/utils.js";
export let config; /* Svelte 5 Props */
export let message; let {
export let displayMessage; config,
export let username; message = $bindable(),
export let isApi; displayMessage = $bindable(),
username,
isApi,
updateSetting
} = $props();
let jwt = ""; /* State Init */
let jwt = $state("");
let displayCheck = $state(false);
/* Functions */
function getUserJwt(username) { function getUserJwt(username) {
if (username) { if (username) {
const p = fetchJwt(username); const p = fetchJwt(username);
@@ -43,7 +50,6 @@
} }
} }
let displayCheck = false;
function clipJwt() { function clipJwt() {
displayCheck = true; displayCheck = true;
// Navigator clipboard api needs a secure context (https) // Navigator clipboard api needs a secure context (https)
@@ -71,14 +77,6 @@
displayCheck = false; displayCheck = false;
}, 1000); }, 1000);
} }
const dispatch = createEventDispatcher();
function updateSetting(selector, target) {
dispatch('update-config', {
selector: selector,
target: target
});
}
</script> </script>
<Row cols={isApi ? 3 : 1} class="p-2 g-2"> <Row cols={isApi ? 3 : 1} class="p-2 g-2">
@@ -90,8 +88,10 @@
method="post" method="post"
action="/frontend/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => onsubmit={(e) => updateSetting(e, {
updateSetting("#paging-form", "pag")} selector: "#paging-form",
target: "pag",
})}
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
<CardTitle <CardTitle
@@ -109,7 +109,7 @@
<input type="hidden" name="key" value="job_list_usePaging" /> <input type="hidden" name="key" value="job_list_usePaging" />
<div class="mb-3"> <div class="mb-3">
<div> <div>
{#if config.job_list_usePaging} {#if config?.job_list_usePaging}
<input type="radio" id="true-checked" name="value" value="true" checked /> <input type="radio" id="true-checked" name="value" value="true" checked />
{:else} {:else}
<input type="radio" id="true" name="value" value="true" /> <input type="radio" id="true" name="value" value="true" />
@@ -117,7 +117,7 @@
<label for="true">Paging with selectable count of jobs.</label> <label for="true">Paging with selectable count of jobs.</label>
</div> </div>
<div> <div>
{#if config.job_list_usePaging} {#if config?.job_list_usePaging}
<input type="radio" id="false" name="value" value="false" /> <input type="radio" id="false" name="value" value="false" />
{:else} {:else}
<input type="radio" id="false-checked" name="value" value="false" checked /> <input type="radio" id="false-checked" name="value" value="false" checked />
@@ -137,7 +137,7 @@
<CardBody> <CardBody>
<CardTitle>Generate JWT</CardTitle> <CardTitle>Generate JWT</CardTitle>
{#if jwt} {#if jwt}
<Button color="secondary" on:click={clipJwt()}> <Button color="secondary" onclick={() => clipJwt()}>
Copy JWT to Clipboard Copy JWT to Clipboard
</Button> </Button>
<p class="mt-2"> <p class="mt-2">
@@ -149,7 +149,7 @@
</p> </p>
{/if} {/if}
{:else} {:else}
<Button color="success" on:click={getUserJwt(username)}> <Button color="success" onclick={() => getUserJwt(username)}>
Generate JWT for '{username}' Generate JWT for '{username}'
</Button> </Button>
<p class="mt-2"> <p class="mt-2">
+179 -197
View File
@@ -16,7 +16,6 @@
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
@@ -26,78 +25,77 @@
ButtonDropdown, ButtonDropdown,
Icon, Icon,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import Tag from "./helper/Tag.svelte";
import Info from "./filters/InfoBox.svelte"; import Info from "./filters/InfoBox.svelte";
import Cluster from "./filters/Cluster.svelte"; import Cluster from "./filters/Cluster.svelte";
import JobStates, { allJobStates } from "./filters/JobStates.svelte"; import JobStates, { allJobStates } from "./filters/JobStates.svelte";
import StartTime from "./filters/StartTime.svelte"; import StartTime, { startTimeSelectOptions } from "./filters/StartTime.svelte";
import Tags from "./filters/Tags.svelte";
import Duration from "./filters/Duration.svelte"; import Duration from "./filters/Duration.svelte";
import Energy from "./filters/Energy.svelte"; import Tags from "./filters/Tags.svelte";
import Tag from "./helper/Tag.svelte";
import Resources from "./filters/Resources.svelte"; import Resources from "./filters/Resources.svelte";
import Energy from "./filters/Energy.svelte";
import Statistics from "./filters/Stats.svelte"; import Statistics from "./filters/Stats.svelte";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
export let menuText = null; menuText = null,
export let filterPresets = {}; filterPresets = {},
export let disableClusterSelection = false; disableClusterSelection = false,
export let startTimeQuickSelect = false; startTimeQuickSelect = false,
export let matchedJobs = -2; matchedJobs = -2,
export let showFilter = true; showFilter = true,
applyFilters
const startTimeSelectOptions = [ } = $props();
{ 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 = { const nodeMatchLabels = {
eq: "", eq: "",
contains: " Contains", contains: " Contains",
} }
const filterReset = { const filterReset = {
projectMatch: "contains", // Direct Filters
userMatch: "contains", dbId: [],
jobId: "",
jobIdMatch: "eq", jobIdMatch: "eq",
nodeMatch: "eq", arrayJobId: null,
jobName: "",
// View Filters
project: "",
projectMatch: "contains",
user: "",
userMatch: "contains",
// Filter Modals
cluster: null, cluster: null,
partition: null, partition: null,
states: allJobStates, states: allJobStates,
startTime: { from: null, to: null, range: ""}, startTime: { from: null, to: null, range: ""},
tags: [],
duration: { duration: {
lessThan: null, lessThan: null,
moreThan: null, moreThan: null,
from: null, from: null,
to: null, to: null,
}, },
dbId: [], tags: [],
jobId: "",
arrayJobId: null,
user: "",
project: "",
jobName: "",
node: null,
energy: { from: null, to: null },
numNodes: { from: null, to: null }, numNodes: { from: null, to: null },
numHWThreads: { from: null, to: null }, numHWThreads: { from: null, to: null },
numAccelerators: { from: null, to: null }, numAccelerators: { from: null, to: null },
node: null,
nodeMatch: "eq",
energy: { from: null, to: null },
stats: [], stats: [],
}; };
let filters = { /* State Init */
projectMatch: filterPresets.projectMatch || "contains", let filters = $state({
userMatch: filterPresets.userMatch || "contains", dbId: filterPresets.dbId || [],
jobId: filterPresets.jobId || "",
jobIdMatch: filterPresets.jobIdMatch || "eq", jobIdMatch: filterPresets.jobIdMatch || "eq",
nodeMatch: filterPresets.nodeMatch || "eq", arrayJobId: filterPresets.arrayJobId || null,
jobName: filterPresets.jobName || "",
project: filterPresets.project || "",
projectMatch: filterPresets.projectMatch || "contains",
user: filterPresets.user || "",
userMatch: filterPresets.userMatch || "contains",
cluster: filterPresets.cluster || null, cluster: filterPresets.cluster || null,
partition: filterPresets.partition || null, partition: filterPresets.partition || null,
states: states:
@@ -105,41 +103,33 @@
? [filterPresets.state].flat() ? [filterPresets.state].flat()
: allJobStates, : allJobStates,
startTime: filterPresets.startTime || { from: null, to: null, range: ""}, startTime: filterPresets.startTime || { from: null, to: null, range: ""},
tags: filterPresets.tags || [],
duration: filterPresets.duration || { duration: filterPresets.duration || {
lessThan: null, lessThan: null,
moreThan: null, moreThan: null,
from: null, from: null,
to: null, to: null,
}, },
dbId: filterPresets.dbId || [], tags: filterPresets.tags || [],
jobId: filterPresets.jobId || "",
arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || "",
project: filterPresets.project || "",
jobName: filterPresets.jobName || "",
node: filterPresets.node || null,
energy: filterPresets.energy || { from: null, to: null },
numNodes: filterPresets.numNodes || { from: null, to: null }, numNodes: filterPresets.numNodes || { from: null, to: null },
numHWThreads: filterPresets.numHWThreads || { from: null, to: null }, numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
numAccelerators: filterPresets.numAccelerators || { from: null, to: null }, numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
node: filterPresets.node || null,
nodeMatch: filterPresets.nodeMatch || "eq",
energy: filterPresets.energy || { from: null, to: null },
stats: filterPresets.stats || [], stats: filterPresets.stats || [],
}; });
let isClusterOpen = false, /* Opened States */
isJobStatesOpen = false, let isClusterOpen = $state(false)
isStartTimeOpen = false, let isJobStatesOpen = $state(false)
isTagsOpen = false, let isStartTimeOpen = $state(false)
isDurationOpen = false, let isDurationOpen = $state(false)
isEnergyOpen = false, let isTagsOpen = $state(false)
isResourcesOpen = false, let isResourcesOpen = $state(false)
isStatsOpen = false, let isEnergyOpen = $state(false)
isNodesModified = false, let isStatsOpen = $state(false)
isHwthreadsModified = false,
isAccsModified = false;
/* Functions */
// Can be called from the outside to trigger a 'update' event from this component. // Can be called from the outside to trigger a 'update' event from this component.
// 'force' option empties existing filters and then applies only 'additionalFilters' // 'force' option empties existing filters and then applies only 'additionalFilters'
export function updateFilters(additionalFilters = null, force = false) { export function updateFilters(additionalFilters = null, force = false) {
@@ -153,10 +143,20 @@
} }
// Construct New Filter // Construct New Filter
let items = []; let items = [];
if (filters.dbId.length != 0)
items.push({ dbId: filters.dbId });
if (filters.jobId)
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
if (filters.arrayJobId != null)
items.push({ arrayJobId: filters.arrayJobId });
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
if (filters.project)
items.push({ project: { [filters.projectMatch]: filters.project } });
if (filters.user)
items.push({ user: { [filters.userMatch]: filters.user } });
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } }); if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
if (filters.partition) items.push({ partition: { eq: filters.partition } }); if (filters.partition) items.push({ partition: { eq: filters.partition } });
if (filters.states.length != allJobStates.length) if (filters.states.length != allJobStates?.length)
items.push({ state: filters.states }); items.push({ state: filters.states });
if (filters.startTime.from || filters.startTime.to) if (filters.startTime.from || filters.startTime.to)
items.push({ items.push({
@@ -166,7 +166,6 @@
items.push({ items.push({
startTime: { range: filters.startTime.range }, startTime: { range: filters.startTime.range },
}); });
if (filters.tags.length != 0) items.push({ tags: filters.tags });
if (filters.duration.from || filters.duration.to) if (filters.duration.from || filters.duration.to)
items.push({ items.push({
duration: { from: filters.duration.from, to: filters.duration.to }, duration: { from: filters.duration.from, to: filters.duration.to },
@@ -175,21 +174,11 @@
items.push({ duration: { from: 0, to: filters.duration.lessThan } }); items.push({ duration: { from: 0, to: filters.duration.lessThan } });
if (filters.duration.moreThan) if (filters.duration.moreThan)
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes
if (filters.energy.from || filters.energy.to) if (filters.tags.length != 0) items.push({ tags: filters.tags });
items.push({
energy: { from: filters.energy.from, to: filters.energy.to },
});
if (filters.dbId.length != 0)
items.push({ dbId: filters.dbId });
if (filters.jobId)
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
if (filters.arrayJobId != null)
items.push({ arrayJobId: filters.arrayJobId });
if (filters.numNodes.from != null || filters.numNodes.to != null) { if (filters.numNodes.from != null || filters.numNodes.to != null) {
items.push({ items.push({
numNodes: { from: filters.numNodes.from, to: filters.numNodes.to }, numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
}); });
isNodesModified = true;
} }
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) { if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) {
items.push({ items.push({
@@ -198,7 +187,6 @@
to: filters.numHWThreads.to, to: filters.numHWThreads.to,
}, },
}); });
isHwthreadsModified = true;
} }
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) { if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) {
items.push({ items.push({
@@ -207,17 +195,16 @@
to: filters.numAccelerators.to, to: filters.numAccelerators.to,
}, },
}); });
isAccsModified = true;
} }
if (filters.user) if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
items.push({ user: { [filters.userMatch]: filters.user } }); if (filters.energy.from || filters.energy.to)
if (filters.project) items.push({
items.push({ project: { [filters.projectMatch]: filters.project } }); energy: { from: filters.energy.from, to: filters.energy.to },
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } }); });
if (filters.stats.length != 0) if (filters.stats.length != 0)
items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) }); items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) });
dispatch("update-filters", { filters: items }); applyFilters({ filters: items });
changeURL(); changeURL();
return items; return items;
} }
@@ -226,20 +213,7 @@
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000); const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
let opts = []; let opts = [];
if (filters.cluster) opts.push(`cluster=${filters.cluster}`); // Direct Filters
if (filters.node) opts.push(`node=${filters.node}`);
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
opts.push(`nodeMatch=${filters.nodeMatch}`);
if (filters.partition) opts.push(`partition=${filters.partition}`);
if (filters.states.length != allJobStates.length)
for (let state of filters.states) opts.push(`state=${state}`);
if (filters.startTime.from && filters.startTime.to)
opts.push(
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
);
if (filters.startTime.range) {
opts.push(`startTime=${filters.startTime.range}`)
}
if (filters.dbId.length != 0) { if (filters.dbId.length != 0) {
for (let dbi of filters.dbId) { for (let dbi of filters.dbId) {
opts.push(`dbId=${dbi}`); opts.push(`dbId=${dbi}`);
@@ -254,21 +228,12 @@
} }
if (filters.jobIdMatch != "eq") if (filters.jobIdMatch != "eq")
opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case
for (let tag of filters.tags) opts.push(`tag=${tag}`); if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
if (filters.duration.from && filters.duration.to) if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`); // View Filters
if (filters.duration.lessThan) if (filters.project) opts.push(`project=${filters.project}`);
opts.push(`duration=0-${filters.duration.lessThan}`); if (filters.project && filters.projectMatch != "contains") // "contains" is default-case
if (filters.duration.moreThan) opts.push(`projectMatch=${filters.projectMatch}`);
opts.push(`duration=${filters.duration.moreThan}-604800`);
if (filters.energy.from && filters.energy.to)
opts.push(`energy=${filters.energy.from}-${filters.energy.to}`);
if (filters.numNodes.from && filters.numNodes.to)
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
if (filters.numHWThreads.from && filters.numHWThreads.to)
opts.push(`numHWThreads=${filters.numHWThreads.from}-${filters.numHWThreads.to}`);
if (filters.numAccelerators.from && filters.numAccelerators.to)
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`);
if (filters.user.length != 0) if (filters.user.length != 0)
if (filters.userMatch != "in") { if (filters.userMatch != "in") {
opts.push(`user=${filters.user}`); opts.push(`user=${filters.user}`);
@@ -277,16 +242,42 @@
} }
if (filters.userMatch != "contains") // "contains" is default-case if (filters.userMatch != "contains") // "contains" is default-case
opts.push(`userMatch=${filters.userMatch}`); opts.push(`userMatch=${filters.userMatch}`);
if (filters.project) opts.push(`project=${filters.project}`); // Filter Modals
if (filters.project && filters.projectMatch != "contains") // "contains" is default-case if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
opts.push(`projectMatch=${filters.projectMatch}`); if (filters.partition) opts.push(`partition=${filters.partition}`);
if (filters.jobName) opts.push(`jobName=${filters.jobName}`); if (filters.states.length != allJobStates?.length)
if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`); for (let state of filters.states) opts.push(`state=${state}`);
if (filters.startTime.from && filters.startTime.to)
opts.push(
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
);
if (filters.startTime.range) {
opts.push(`startTime=${filters.startTime.range}`)
}
if (filters.duration.from && filters.duration.to)
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
if (filters.duration.lessThan)
opts.push(`duration=0-${filters.duration.lessThan}`);
if (filters.duration.moreThan)
opts.push(`duration=${filters.duration.moreThan}-604800`);
if (filters.tags.length != 0)
for (let tag of filters.tags) opts.push(`tag=${tag}`);
if (filters.numNodes.from && filters.numNodes.to)
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
if (filters.numHWThreads.from && filters.numHWThreads.to)
opts.push(`numHWThreads=${filters.numHWThreads.from}-${filters.numHWThreads.to}`);
if (filters.numAccelerators.from && filters.numAccelerators.to)
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`);
if (filters.node) opts.push(`node=${filters.node}`);
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
opts.push(`nodeMatch=${filters.nodeMatch}`);
if (filters.energy.from && filters.energy.to)
opts.push(`energy=${filters.energy.from}-${filters.energy.to}`);
if (filters.stats.length != 0) if (filters.stats.length != 0)
for (let stat of filters.stats) { for (let stat of filters.stats) {
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`); opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
} }
// Build && Return
if (opts.length == 0 && window.location.search.length <= 1) return; if (opts.length == 0 && window.location.search.length <= 1) return;
let newurl = `${window.location.pathname}?${opts.join("&")}`; let newurl = `${window.location.pathname}?${opts.join("&")}`;
window.history.replaceState(null, "", newurl); window.history.replaceState(null, "", newurl);
@@ -296,7 +287,7 @@
<!-- Dropdown-Button --> <!-- Dropdown-Button -->
<ButtonGroup> <ButtonGroup>
{#if showFilter} {#if showFilter}
<ButtonDropdown class="cc-dropdown-on-hover mb-1" style="{(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}"> <ButtonDropdown class="cc-dropdown-on-hover mb-1" style={(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}>
<DropdownToggle outline caret color="success"> <DropdownToggle outline caret color="success">
<Icon name="sliders" /> <Icon name="sliders" />
Filters Filters
@@ -307,36 +298,36 @@
<DropdownItem disabled>{menuText}</DropdownItem> <DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider /> <DropdownItem divider />
{/if} {/if}
<DropdownItem on:click={() => (isClusterOpen = true)}> <DropdownItem onclick={() => (isClusterOpen = true)}>
<Icon name="cpu" /> Cluster/Partition <Icon name="cpu" /> Cluster/Partition
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}> <DropdownItem onclick={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill" /> Job States <Icon name="gear-fill" /> Job States
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}> <DropdownItem onclick={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range" /> Start Time <Icon name="calendar-range" /> Start Time
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}> <DropdownItem onclick={() => (isDurationOpen = true)}>
<Icon name="stopwatch" /> Duration <Icon name="stopwatch" /> Duration
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}> <DropdownItem onclick={() => (isTagsOpen = true)}>
<Icon name="tags" /> Tags <Icon name="tags" /> Tags
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}> <DropdownItem onclick={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack" /> Resources <Icon name="hdd-stack" /> Resources
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isEnergyOpen = true)}> <DropdownItem onclick={() => (isEnergyOpen = true)}>
<Icon name="lightning-charge-fill" /> Energy <Icon name="lightning-charge-fill" /> Energy
</DropdownItem> </DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}> <DropdownItem onclick={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics <Icon name="bar-chart" onclick={() => (isStatsOpen = true)} /> Statistics
</DropdownItem> </DropdownItem>
{#if startTimeQuickSelect} {#if startTimeQuickSelect}
<DropdownItem divider /> <DropdownItem divider />
<DropdownItem disabled>Start Time Quick Selection</DropdownItem> <DropdownItem disabled>Start Time Quick Selection</DropdownItem>
{#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }} {#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
<DropdownItem <DropdownItem
on:click={() => { onclick={() => {
filters.startTime.from = null filters.startTime.from = null
filters.startTime.to = null filters.startTime.to = null
filters.startTime.range = range; filters.startTime.range = range;
@@ -353,7 +344,7 @@
{/if} {/if}
{#if matchedJobs >= -1} {#if matchedJobs >= -1}
<Button class="mb-1" style="margin-right: 0.5rem;" disabled outline> <Button class="mb-1" style="margin-right: 0.25rem;" disabled outline>
{matchedJobs == -1 ? 'Loading ...' : `${matchedJobs} jobs`} {matchedJobs == -1 ? 'Loading ...' : `${matchedJobs} jobs`}
</Button> </Button>
{/if} {/if}
@@ -362,7 +353,7 @@
{#if showFilter} {#if showFilter}
<!-- SELECTED FILTER PILLS --> <!-- SELECTED FILTER PILLS -->
{#if filters.cluster} {#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}> <Info icon="cpu" onclick={() => (isClusterOpen = true)}>
{filters.cluster} {filters.cluster}
{#if filters.partition} {#if filters.partition}
({filters.partition}) ({filters.partition})
@@ -370,14 +361,14 @@
</Info> </Info>
{/if} {/if}
{#if filters.states.length != allJobStates.length} {#if filters.states.length != allJobStates?.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}> <Info icon="gear-fill" onclick={() => (isJobStatesOpen = true)}>
{filters.states.join(", ")} {filters.states.join(", ")}
</Info> </Info>
{/if} {/if}
{#if filters.startTime.from || filters.startTime.to} {#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}> <Info icon="calendar-range" onclick={() => (isStartTimeOpen = true)}>
{new Date(filters.startTime.from).toLocaleString()} - {new Date( {new Date(filters.startTime.from).toLocaleString()} - {new Date(
filters.startTime.to, filters.startTime.to,
).toLocaleString()} ).toLocaleString()}
@@ -385,13 +376,13 @@
{/if} {/if}
{#if filters.startTime.range} {#if filters.startTime.range}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}> <Info icon="calendar-range" onclick={() => (isStartTimeOpen = true)}>
{startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel } {startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel }
</Info> </Info>
{/if} {/if}
{#if filters.duration.from || filters.duration.to} {#if filters.duration.from || filters.duration.to}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
{Math.floor(filters.duration.from / 3600)}h:{Math.floor( {Math.floor(filters.duration.from / 3600)}h:{Math.floor(
(filters.duration.from % 3600) / 60, (filters.duration.from % 3600) / 60,
)}m - )}m -
@@ -402,7 +393,7 @@
{/if} {/if}
{#if filters.duration.lessThan} {#if filters.duration.lessThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
Duration less than {Math.floor( Duration less than {Math.floor(
filters.duration.lessThan / 3600, filters.duration.lessThan / 3600,
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m )}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
@@ -410,7 +401,7 @@
{/if} {/if}
{#if filters.duration.moreThan} {#if filters.duration.moreThan}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}> <Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
Duration more than {Math.floor( Duration more than {Math.floor(
filters.duration.moreThan / 3600, filters.duration.moreThan / 3600,
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m )}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
@@ -418,47 +409,45 @@
{/if} {/if}
{#if filters.tags.length != 0} {#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}> <Info icon="tags" onclick={() => (isTagsOpen = true)}>
{#each filters.tags as tagId} {#each filters.tags as tagId}
{#key tagId} <Tag id={tagId} clickable={false} />
<Tag id={tagId} clickable={false} />
{/key}
{/each} {/each}
</Info> </Info>
{/if} {/if}
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null} {#if filters.numNodes.from != null || filters.numNodes.to != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> <Info icon="hdd-stack" onclick={() => (isResourcesOpen = true)}>
{#if isNodesModified}
Nodes: {filters.numNodes.from} - {filters.numNodes.to} Nodes: {filters.numNodes.from} - {filters.numNodes.to}
{/if} </Info>
{#if isNodesModified && isHwthreadsModified}, {/if}
{/if}
{#if isHwthreadsModified} {#if filters.numHWThreads.from != null || filters.numHWThreads.to != null}
<Info icon="cpu" onclick={() => (isResourcesOpen = true)}>
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to} HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
{/if} </Info>
{#if (isNodesModified || isHwthreadsModified) && isAccsModified}, {/if}
{/if}
{#if isAccsModified} {#if filters.numAccelerators.from != null || filters.numAccelerators.to != null}
<Info icon="gpu-card" onclick={() => (isResourcesOpen = true)}>
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to} Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
{/if}
</Info> </Info>
{/if} {/if}
{#if filters.node != null} {#if filters.node != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}> <Info icon="hdd-stack" onclick={() => (isResourcesOpen = true)}>
Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node} Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node}
</Info> </Info>
{/if} {/if}
{#if filters.energy.from || filters.energy.to} {#if filters.energy.from || filters.energy.to}
<Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}> <Info icon="lightning-charge-fill" onclick={() => (isEnergyOpen = true)}>
Total Energy: {filters.energy.from} - {filters.energy.to} Total Energy: {filters.energy.from} - {filters.energy.to}
</Info> </Info>
{/if} {/if}
{#if filters.stats.length > 0} {#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}> <Info icon="bar-chart" onclick={() => (isStatsOpen = true)}>
{filters.stats {filters.stats
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`) .map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
.join(", ")} .join(", ")}
@@ -467,69 +456,62 @@
{/if} {/if}
<Cluster <Cluster
{disableClusterSelection}
bind:isOpen={isClusterOpen} bind:isOpen={isClusterOpen}
bind:cluster={filters.cluster} presetCluster={filters.cluster}
bind:partition={filters.partition} presetPartition={filters.partition}
on:set-filter={() => updateFilters()} {disableClusterSelection}
setFilter={(filter) => updateFilters(filter)}
/> />
<JobStates <JobStates
bind:isOpen={isJobStatesOpen} bind:isOpen={isJobStatesOpen}
bind:states={filters.states} presetStates={filters.states}
on:set-filter={() => updateFilters()} setFilter={(filter) => updateFilters(filter)}
/> />
<StartTime <StartTime
bind:isOpen={isStartTimeOpen} bind:isOpen={isStartTimeOpen}
bind:from={filters.startTime.from} presetStartTime={filters.startTime}
bind:to={filters.startTime.to} setFilter={(filter) => updateFilters(filter)}
bind:range={filters.startTime.range}
{startTimeSelectOptions}
on:set-filter={() => updateFilters()}
/> />
<Duration <Duration
bind:isOpen={isDurationOpen} bind:isOpen={isDurationOpen}
bind:lessThan={filters.duration.lessThan} presetDuration={filters.duration}
bind:moreThan={filters.duration.moreThan} setFilter={(filter) => updateFilters(filter)}
bind:from={filters.duration.from}
bind:to={filters.duration.to}
on:set-filter={() => updateFilters()}
/> />
<Tags <Tags
bind:isOpen={isTagsOpen} bind:isOpen={isTagsOpen}
bind:tags={filters.tags} presetTags={filters.tags}
on:set-filter={() => updateFilters()} setFilter={(filter) => updateFilters(filter)}
/> />
<Resources <Resources
cluster={filters.cluster}
bind:isOpen={isResourcesOpen} bind:isOpen={isResourcesOpen}
bind:numNodes={filters.numNodes} activeCluster={filters.cluster}
bind:numHWThreads={filters.numHWThreads} presetNumNodes={filters.numNodes}
bind:numAccelerators={filters.numAccelerators} presetNumHWThreads={filters.numHWThreads}
bind:namedNode={filters.node} presetNumAccelerators={filters.numAccelerators}
bind:nodeMatch={filters.nodeMatch} presetNamedNode={filters.node}
bind:isNodesModified presetNodeMatch={filters.nodeMatch}
bind:isHwthreadsModified setFilter={(filter) => updateFilters(filter)}
bind:isAccsModified
on:set-filter={() => updateFilters()}
/>
<Statistics
bind:isOpen={isStatsOpen}
bind:stats={filters.stats}
on:set-filter={() => updateFilters()}
/> />
<Energy <Energy
bind:isOpen={isEnergyOpen} bind:isOpen={isEnergyOpen}
bind:energy={filters.energy} presetEnergy={filters.energy}
on:set-filter={() => updateFilters()} setFilter={(filter) => updateFilters(filter)}
/> />
<Statistics
bind:isOpen={isStatsOpen}
presetStats={filters.stats}
setFilter={(filter) => updateFilters(filter)}
/>
<style> <style>
:global(.cc-dropdown-on-hover:hover .dropdown-menu) { :global(.cc-dropdown-on-hover:hover .dropdown-menu) {
display: block; display: block;
+90 -90
View File
@@ -23,94 +23,91 @@
import { formatTime, roundTwoDigits } from "./units.js"; import { formatTime, roundTwoDigits } from "./units.js";
import Comparogram from "./plots/Comparogram.svelte"; import Comparogram from "./plots/Comparogram.svelte";
const ccconfig = getContext("cc-config"), /* Svelte 5 Props */
// initialized = getContext("initialized"), let {
globalMetrics = getContext("globalMetrics"); matchedCompareJobs = $bindable(0),
metrics = $bindable(ccconfig?.plot_list_selectedMetrics),
filterBuffer = [],
} = $props();
export let matchedCompareJobs = 0; /* Const Init */
export let metrics = ccconfig.plot_list_selectedMetrics; const client = getContextClient();
export let filterBuffer = []; const ccconfig = getContext("cc-config");
const globalMetrics = getContext("globalMetrics");
let filter = [...filterBuffer] || []; // const initialized = getContext("initialized");
let comparePlotData = {};
let compareTableData = [];
let compareTableSorting = {};
let jobIds = [];
let jobClusters = [];
let tableJobIDFilter = "";
/*uPlot*/
let plotSync = uPlot.sync("compareJobsView");
/* GQL */
const client = getContextClient();
// Pull All Series For Metrics Statistics Only On Node Scope // Pull All Series For Metrics Statistics Only On Node Scope
const compareQuery = gql` const compareQuery = gql`
query ($filter: [JobFilter!]!, $metrics: [String!]!) { query ($filter: [JobFilter!]!, $metrics: [String!]!) {
jobsMetricStats(filter: $filter, metrics: $metrics) { jobsMetricStats(filter: $filter, metrics: $metrics) {
id id
jobId jobId
startTime startTime
duration duration
cluster cluster
subCluster subCluster
numNodes numNodes
numHWThreads numHWThreads
numAccelerators numAccelerators
stats { stats {
name name
data { data {
min min
avg avg
max max
}
} }
} }
} }
}
`; `;
/* REACTIVES */ /* Var Init*/
let plotSync = uPlot.sync("compareJobsView");
$: compareData = queryStore({ /* State Init */
client: client, let filter = $state([...filterBuffer] || []);
query: compareQuery, let tableJobIDFilter = $state("");
variables:{ filter, metrics },
});
$: matchedCompareJobs = $compareData.data != null ? $compareData.data.jobsMetricStats.length : -1; /* Derived*/
const compareData = $derived(queryStore({
$: if ($compareData.data != null) { client: client,
jobIds = []; query: compareQuery,
jobClusters = []; variables:{ filter, metrics },
comparePlotData = {}; })
compareTableData = [...$compareData.data.jobsMetricStats]; );
jobs2uplot($compareData.data.jobsMetricStats, metrics); let jobIds = $derived($compareData?.data ? $compareData.data.jobsMetricStats.map((jms) => jms.jobId) : []);
} let jobClusters = $derived($compareData?.data ? $compareData.data.jobsMetricStats.map((jms) => `${jms.cluster} ${jms.subCluster}`) : []);
let compareTableData = $derived($compareData?.data ? [...$compareData.data.jobsMetricStats] : []);
$: if ((!$compareData.fetching && !$compareData.error) && metrics) { let comparePlotData = $derived($compareData?.data ? jobs2uplot($compareData.data.jobsMetricStats, metrics) : {});
let compareTableSorting = $derived.by(() => {
let pendingSort = {};
// Meta // Meta
compareTableSorting['meta'] = { pendingSort['meta'] = {
startTime: { dir: "down", active: true }, startTime: { dir: "down", active: true },
duration: { dir: "up", active: false }, duration: { dir: "up", active: false },
cluster: { dir: "up", active: false }, cluster: { dir: "up", active: false },
}; };
// Resources // Resources
compareTableSorting['resources'] = { pendingSort['resources'] = {
Nodes: { dir: "up", active: false }, Nodes: { dir: "up", active: false },
Threads: { dir: "up", active: false }, Threads: { dir: "up", active: false },
Accs: { dir: "up", active: false }, Accs: { dir: "up", active: false },
}; };
// Metrics
for (let metric of metrics) { for (let metric of metrics) {
compareTableSorting[metric] = { pendingSort[metric] = {
min: { dir: "up", active: false }, min: { dir: "up", active: false },
avg: { dir: "up", active: false }, avg: { dir: "up", active: false },
max: { dir: "up", active: false }, max: { dir: "up", active: false },
}; };
} }
} return pendingSort;
});
/* FUNCTIONS */ /* Effect */
$effect(() => {
matchedCompareJobs = $compareData?.data != null ? $compareData.data.jobsMetricStats.length : -1;
});
/* Functions */
// (Re-)query and optionally set new filters; Query will be started reactively. // (Re-)query and optionally set new filters; Query will be started reactively.
export function queryJobs(filters) { export function queryJobs(filters) {
if (filters != null) { if (filters != null) {
@@ -133,6 +130,7 @@
} }
compareTableSorting = { ...compareTableSorting }; compareTableSorting = { ...compareTableSorting };
let pendingCompareData;
if (key == 'resources') { if (key == 'resources') {
let longField = ""; let longField = "";
switch (field) { switch (field) {
@@ -148,12 +146,13 @@
default: default:
console.log("Unknown Res Field", field) console.log("Unknown Res Field", field)
} }
compareTableData = compareTableData.sort((j1, j2) => {
pendingCompareData = compareTableData.sort((j1, j2) => {
if (j1[longField] == null || j2[longField] == null) return -1; if (j1[longField] == null || j2[longField] == null) return -1;
return s.dir != "up" ? j1[longField] - j2[longField] : j2[longField] - j1[longField]; return s.dir != "up" ? j1[longField] - j2[longField] : j2[longField] - j1[longField];
}); });
} else if (key == 'meta') { } else if (key == 'meta') {
compareTableData = compareTableData.sort((j1, j2) => { pendingCompareData = compareTableData.sort((j1, j2) => {
if (j1[field] == null || j2[field] == null) return -1; if (j1[field] == null || j2[field] == null) return -1;
if (field == 'cluster') { if (field == 'cluster') {
let c1 = `${j1.cluster} (${j1.subCluster})` let c1 = `${j1.cluster} (${j1.subCluster})`
@@ -164,52 +163,54 @@
} }
}); });
} else { } else {
compareTableData = compareTableData.sort((j1, j2) => { pendingCompareData = compareTableData.sort((j1, j2) => {
let s1 = j1.stats.find((m) => m.name == key)?.data; let s1 = j1.stats.find((m) => m.name == key)?.data;
let s2 = j2.stats.find((m) => m.name == key)?.data; let s2 = j2.stats.find((m) => m.name == key)?.data;
if (s1 == null || s2 == null) return -1; if (s1 == null || s2 == null) return -1;
return s.dir != "up" ? s1[field] - s2[field] : s2[field] - s1[field]; return s.dir != "up" ? s1[field] - s2[field] : s2[field] - s1[field];
}); });
} }
compareTableData = [...pendingCompareData]
} }
function jobs2uplot(jobs, metrics) { function jobs2uplot(jobs, metrics) {
// Proxy Init
let pendingComparePlotData = {};
// Resources Init // Resources Init
comparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS] pendingComparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS]
// Metric Init // Metric Init
for (let m of metrics) { for (let m of metrics) {
// Get Unit // Get Unit
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "") const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
comparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX] pendingComparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX]
} }
// Iterate jobs if exists // Iterate jobs if exists
if (jobs) { if (jobs) {
let plotIndex = 0 let plotIndex = 0
jobs.forEach((j) => { jobs.forEach((j) => {
// Collect JobIDs & Clusters for X-Ticks and Legend
jobIds.push(j.jobId)
jobClusters.push(`${j.cluster} ${j.subCluster}`)
// Resources // Resources
comparePlotData['resources'].data[0].push(plotIndex) pendingComparePlotData['resources'].data[0].push(plotIndex)
comparePlotData['resources'].data[1].push(j.startTime) pendingComparePlotData['resources'].data[1].push(j.startTime)
comparePlotData['resources'].data[2].push(j.duration) pendingComparePlotData['resources'].data[2].push(j.duration)
comparePlotData['resources'].data[3].push(j.numNodes) pendingComparePlotData['resources'].data[3].push(j.numNodes)
comparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0) pendingComparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
comparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0) pendingComparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
// Metrics // Metrics
for (let s of j.stats) { for (let s of j.stats) {
comparePlotData[s.name].data[0].push(plotIndex) pendingComparePlotData[s.name].data[0].push(plotIndex)
comparePlotData[s.name].data[1].push(j.startTime) pendingComparePlotData[s.name].data[1].push(j.startTime)
comparePlotData[s.name].data[2].push(j.duration) pendingComparePlotData[s.name].data[2].push(j.duration)
comparePlotData[s.name].data[3].push(s.data.min) pendingComparePlotData[s.name].data[3].push(s.data.min)
comparePlotData[s.name].data[4].push(s.data.avg) pendingComparePlotData[s.name].data[4].push(s.data.avg)
comparePlotData[s.name].data[5].push(s.data.max) pendingComparePlotData[s.name].data[5].push(s.data.max)
} }
plotIndex++ plotIndex++
}) })
} }
return {...pendingComparePlotData};
} }
// Adapt for Persisting Job Selections in DB later down the line // Adapt for Persisting Job Selections in DB later down the line
@@ -238,7 +239,6 @@
// } // }
// }); // });
// } // }
</script> </script>
{#if $compareData.fetching} {#if $compareData.fetching}
@@ -265,7 +265,7 @@
xticks={jobIds} xticks={jobIds}
xinfo={jobClusters} xinfo={jobClusters}
ylabel={'Resource Counts'} ylabel={'Resource Counts'}
data={comparePlotData['resources'].data} data={comparePlotData['resources']?.data}
{plotSync} {plotSync}
forResources forResources
/> />
@@ -281,8 +281,8 @@
xinfo={jobClusters} xinfo={jobClusters}
ylabel={m} ylabel={m}
metric={m} metric={m}
yunit={comparePlotData[m].unit} yunit={comparePlotData[m]?.unit}
data={comparePlotData[m].data} data={comparePlotData[m]?.data}
{plotSync} {plotSync}
/> />
</Col> </Col>
@@ -314,7 +314,7 @@
</InputGroupText> </InputGroupText>
</InputGroup> </InputGroup>
</th> </th>
<th on:click={() => sortBy('meta', 'startTime')}> <th onclick={() => sortBy('meta', 'startTime')}>
Sort Sort
<Icon <Icon
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime'] name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
@@ -323,7 +323,7 @@
: ''}" : ''}"
/> />
</th> </th>
<th on:click={() => sortBy('meta', 'duration')}> <th onclick={() => sortBy('meta', 'duration')}>
Sort Sort
<Icon <Icon
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration'] name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
@@ -332,7 +332,7 @@
: ''}" : ''}"
/> />
</th> </th>
<th on:click={() => sortBy('meta', 'cluster')}> <th onclick={() => sortBy('meta', 'cluster')}>
Sort Sort
<Icon <Icon
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster'] name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
@@ -342,7 +342,7 @@
/> />
</th> </th>
{#each ["Nodes", "Threads", "Accs"] as res} {#each ["Nodes", "Threads", "Accs"] as res}
<th on:click={() => sortBy('resources', res)}> <th onclick={() => sortBy('resources', res)}>
{res} {res}
<Icon <Icon
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res] name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
@@ -354,7 +354,7 @@
{/each} {/each}
{#each metrics as metric} {#each metrics as metric}
{#each ["min", "avg", "max"] as stat} {#each ["min", "avg", "max"] as stat}
<th on:click={() => sortBy(metric, stat)}> <th onclick={() => sortBy(metric, stat)}>
{stat.charAt(0).toUpperCase() + stat.slice(1)} {stat.charAt(0).toUpperCase() + stat.slice(1)}
<Icon <Icon
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat] name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]
+13 -8
View File
@@ -113,11 +113,6 @@
variables: { paging, sorting, filter }, variables: { paging, sorting, filter },
}); });
$: if (!usePaging && sorting) {
// console.log('Reset Paging ...')
paging = { itemsPerPage: 10, page: 1 }
};
let jobs = []; let jobs = [];
$: if ($initialized && $jobsStore.data) { $: if ($initialized && $jobsStore.data) {
if (usePaging) { if (usePaging) {
@@ -143,10 +138,20 @@
} }
} }
$: if (!usePaging && (sorting || filter)) {
// Continous Scroll: Reset list and paging if parameters change: Existing entries will not match new selections
jobs = [];
paging = { itemsPerPage: 10, page: 1 };
}
$: matchedListJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1; $: matchedListJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity) // Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refreshJobs() { export function refreshJobs() {
if (!usePaging) {
jobs = []; // Empty Joblist before refresh, prevents infinite buildup
paging = { itemsPerPage: 10, page: 1 };
}
jobsStore = queryStore({ jobsStore = queryStore({
client: client, client: client,
query: query, query: query,
@@ -285,7 +290,7 @@
</td> </td>
</tr> </tr>
{:else} {:else}
{#each jobs as job (job)} {#each jobs as job (job.id)}
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)} <JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]} on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]}
on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)} on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)}
@@ -312,11 +317,11 @@
{#if usePaging} {#if usePaging}
<Pagination <Pagination
bind:page {page}
{itemsPerPage} {itemsPerPage}
itemText="Jobs" itemText="Jobs"
totalItems={matchedListJobs} totalItems={matchedListJobs}
on:update-paging={({ detail }) => { updatePaging={(detail) => {
if (detail.itemsPerPage != itemsPerPage) { if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page); updateConfiguration(detail.itemsPerPage.toString(), detail.page);
} else { } else {
+12
View File
@@ -14,6 +14,18 @@
export let itemsPerRow export let itemsPerRow
export let items export let items
/* Migtation Notes
* Requirements
* - Parent Components must be already Migrated
* - TODO: Job.root.svelte, Node.root.svelte
* - DONE: Analysis, Status, User
*
* How-To
* - Define "Plot-Slotcode" as SV5 Snippet with argument "item" in parent (!)
* - Pass new snippet as argument/prop to here
* - @render snippet in items-loop with argument == item
*/
</script> </script>
<Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}> <Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}>
-59
View File
@@ -1,59 +0,0 @@
<!--
@component Organized display of plots as table
Properties:
- `itemsPerRow Number`: Elements to render per row
- `items [Any]`: List of plot components to render
- `padding Number`: Padding between plot elements
- `renderFor String`: If 'job', filter disabled metrics
-->
<script>
export let itemsPerRow
export let items
export let padding = 10
export let renderFor
let rows = []
let tableWidth = 0
const isPlaceholder = x => x._is_placeholder === true
function tile(items, itemsPerRow) {
const rows = []
for (let ri = 0; ri < items.length; ri += itemsPerRow) {
const row = []
for (let ci = 0; ci < itemsPerRow; ci += 1) {
if (ri + ci < items.length)
row.push(items[ri + ci])
else
row.push({ _is_placeholder: true, ri, ci })
}
rows.push(row)
}
return rows
}
$: if (renderFor === 'job') {
rows = tile(items.filter(item => item.disabled === false), itemsPerRow)
} else {
rows = tile(items, itemsPerRow)
}
$: plotWidth = (tableWidth / itemsPerRow) - (padding * itemsPerRow)
</script>
<table bind:clientWidth={tableWidth} style="width: 100%; table-layout: fixed;">
{#each rows as row}
<tr>
{#each row as item (item)}
<td style="vertical-align:top;"> <!-- For Aligning Notice Cards -->
{#if !isPlaceholder(item) && plotWidth > 0}
<slot item={item} width={plotWidth}></slot>
{/if}
</td>
{/each}
</tr>
{/each}
</table>
+37 -32
View File
@@ -13,7 +13,7 @@
--> -->
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { getContext } from "svelte";
import { import {
Button, Button,
ListGroup, ListGroup,
@@ -24,18 +24,23 @@
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
const clusters = getContext("clusters"), /* Svelte 5 Props */
initialized = getContext("initialized"), let {
dispatch = createEventDispatcher(); isOpen = $bindable(false),
presetCluster = "",
presetPartition = "",
disableClusterSelection = false,
setFilter
} = $props();
/* State Init */
let pendingCluster = $state(presetCluster);
let pendingPartition = $state(presetPartition);
/* Derived Vars */
const clusters = $derived(getContext("clusters"));
const initialized = $derived(getContext("initialized"));
export let disableClusterSelection = false;
export let isModified = false;
export let isOpen = false;
export let cluster = null;
export let partition = null;
let pendingCluster = cluster,
pendingPartition = partition;
$: isModified = pendingCluster != cluster || pendingPartition != partition;
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@@ -45,13 +50,13 @@
<h4>Cluster</h4> <h4>Cluster</h4>
{#if disableClusterSelection} {#if disableClusterSelection}
<Button color="info" class="w-100 mb-2" disabled><b>Info: Cluster Selection Disabled in This View</b></Button> <Button color="info" class="w-100 mb-2" disabled><b>Info: Cluster Selection Disabled in This View</b></Button>
<Button outline color="primary" class="w-100 mb-2" disabled><b>Selected Cluster: {cluster}</b></Button> <Button outline color="primary" class="w-100 mb-2" disabled><b>Selected Cluster: {presetCluster}</b></Button>
{:else} {:else}
<ListGroup> <ListGroup>
<ListGroupItem <ListGroupItem
disabled={disableClusterSelection} disabled={disableClusterSelection}
active={pendingCluster == null} active={pendingCluster == null}
on:click={() => ((pendingCluster = null), (pendingPartition = null))} onclick={() => ((pendingCluster = null), (pendingPartition = null))}
> >
Any Cluster Any Cluster
</ListGroupItem> </ListGroupItem>
@@ -59,7 +64,7 @@
<ListGroupItem <ListGroupItem
disabled={disableClusterSelection} disabled={disableClusterSelection}
active={pendingCluster == cluster.name} active={pendingCluster == cluster.name}
on:click={() => ( onclick={() => (
(pendingCluster = cluster.name), (pendingPartition = null) (pendingCluster = cluster.name), (pendingPartition = null)
)} )}
> >
@@ -75,14 +80,14 @@
<ListGroup> <ListGroup>
<ListGroupItem <ListGroupItem
active={pendingPartition == null} active={pendingPartition == null}
on:click={() => (pendingPartition = null)} onclick={() => (pendingPartition = null)}
> >
Any Partition Any Partition
</ListGroupItem> </ListGroupItem>
{#each clusters.find((c) => c.name == pendingCluster).partitions as partition} {#each clusters?.find((c) => c.name == pendingCluster)?.partitions as partition}
<ListGroupItem <ListGroupItem
active={pendingPartition == partition} active={pendingPartition == partition}
on:click={() => (pendingPartition = partition)} onclick={() => (pendingPartition = partition)}
> >
{partition} {partition}
</ListGroupItem> </ListGroupItem>
@@ -93,22 +98,22 @@
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
cluster = pendingCluster; setFilter({ cluster: pendingCluster, partition: pendingPartition });
partition = pendingPartition;
dispatch("set-filter", { cluster, partition });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button {#if !disableClusterSelection}
color="danger" <Button
on:click={() => { color="danger"
isOpen = false; onclick={() => {
cluster = pendingCluster = null; isOpen = false;
partition = pendingPartition = null; pendingCluster = null;
dispatch("set-filter", { cluster, partition }); pendingPartition = null;
}}>Reset</Button setFilter({ cluster: pendingCluster, partition: pendingPartition})
> }}>Reset</Button
<Button on:click={() => (isOpen = false)}>Close</Button> >
{/if}
<Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
@@ -13,7 +13,6 @@
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
Row, Row,
Col, Col,
@@ -24,61 +23,81 @@
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
isOpen = $bindable(false),
presetDuration ={lessThan: null, moreThan: null, from: null, to: null},
setFilter
} = $props();
export let isOpen = false; /* State Init */
export let lessThan = null; let pendingDuration = $state(presetDuration);
export let moreThan = null; let lessState = $state(secsToHoursAndMins(presetDuration?.lessThan));
export let from = null; let moreState = $state(secsToHoursAndMins(presetDuration?.moreThan));
export let to = null; let fromState = $state(secsToHoursAndMins(presetDuration?.from));
let toState = $state(secsToHoursAndMins(presetDuration?.to));
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo; /* Derived Init */
let lessDisabled = false, const lessDisabled = $derived(
moreDisabled = false, moreState.hours !== 0 ||
betweenDisabled = false; moreState.mins !== 0 ||
fromState.hours !== 0 ||
fromState.mins !== 0 ||
toState.hours !== 0 ||
toState.mins !== 0
);
function reset() { const moreDisabled = $derived(
pendingLessThan = lessState.hours !== 0 ||
lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan); lessState.mins !== 0 ||
pendingMoreThan = fromState.hours !== 0 ||
moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan); fromState.mins !== 0 ||
pendingFrom = toState.hours !== 0 ||
from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from); toState.mins !== 0
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to); );
const betweenDisabled = $derived(
moreState.hours !== 0 ||
moreState.mins !== 0 ||
lessState.hours !== 0 ||
lessState.mins !== 0
)
/* Functions */
function resetPending() {
pendingDuration = {
lessThan: null,
moreThan: null,
from: null,
to: null
}
};
function resetStates() {
lessState = { hours: 0, mins: 0 }
moreState = { hours: 0, mins: 0 }
fromState = { hours: 0, mins: 0 }
toState = { hours: 0, mins: 0 }
};
function secsToHoursAndMins(seconds) {
if (seconds) {
const hours = Math.floor(seconds / 3600);
seconds -= hours * 3600;
const mins = Math.floor(seconds / 60);
return { hours, mins };
} else {
return { hours: 0, mins: 0 }
}
} }
reset(); function hoursAndMinsToSecs(hoursAndMins) {
if (hoursAndMins) {
function secsToHoursAndMins(duration) { return hoursAndMins.hours * 3600 + hoursAndMins.mins * 60;
const hours = Math.floor(duration / 3600); } else {
duration -= hours * 3600; return 0
const mins = Math.floor(duration / 60); }
return { hours, mins };
} }
function hoursAndMinsToSecs({ hours, mins }) {
return hours * 3600 + mins * 60;
}
$: lessDisabled =
pendingMoreThan.hours !== 0 ||
pendingMoreThan.mins !== 0 ||
pendingFrom.hours !== 0 ||
pendingFrom.mins !== 0 ||
pendingTo.hours !== 0 ||
pendingTo.mins !== 0;
$: moreDisabled =
pendingLessThan.hours !== 0 ||
pendingLessThan.mins !== 0 ||
pendingFrom.hours !== 0 ||
pendingFrom.mins !== 0 ||
pendingTo.hours !== 0 ||
pendingTo.mins !== 0;
$: betweenDisabled =
pendingMoreThan.hours !== 0 ||
pendingMoreThan.mins !== 0 ||
pendingLessThan.hours !== 0 ||
pendingLessThan.mins !== 0;
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@@ -92,7 +111,7 @@
type="number" type="number"
min="0" min="0"
class="form-control" class="form-control"
bind:value={pendingMoreThan.hours} bind:value={moreState.hours}
disabled={moreDisabled} disabled={moreDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@@ -107,7 +126,7 @@
min="0" min="0"
max="59" max="59"
class="form-control" class="form-control"
bind:value={pendingMoreThan.mins} bind:value={moreState.mins}
disabled={moreDisabled} disabled={moreDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@@ -126,7 +145,7 @@
type="number" type="number"
min="0" min="0"
class="form-control" class="form-control"
bind:value={pendingLessThan.hours} bind:value={lessState.hours}
disabled={lessDisabled} disabled={lessDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@@ -141,7 +160,7 @@
min="0" min="0"
max="59" max="59"
class="form-control" class="form-control"
bind:value={pendingLessThan.mins} bind:value={lessState.mins}
disabled={lessDisabled} disabled={lessDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@@ -160,7 +179,7 @@
type="number" type="number"
min="0" min="0"
class="form-control" class="form-control"
bind:value={pendingFrom.hours} bind:value={fromState.hours}
disabled={betweenDisabled} disabled={betweenDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@@ -175,7 +194,7 @@
min="0" min="0"
max="59" max="59"
class="form-control" class="form-control"
bind:value={pendingFrom.mins} bind:value={fromState.mins}
disabled={betweenDisabled} disabled={betweenDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@@ -192,7 +211,7 @@
type="number" type="number"
min="0" min="0"
class="form-control" class="form-control"
bind:value={pendingTo.hours} bind:value={toState.hours}
disabled={betweenDisabled} disabled={betweenDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@@ -207,7 +226,7 @@
min="0" min="0"
max="59" max="59"
class="form-control" class="form-control"
bind:value={pendingTo.mins} bind:value={toState.mins}
disabled={betweenDisabled} disabled={betweenDisabled}
/> />
<div class="input-group-append"> <div class="input-group-append">
@@ -220,39 +239,32 @@
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
lessThan = hoursAndMinsToSecs(pendingLessThan); pendingDuration.lessThan = hoursAndMinsToSecs(lessState);
moreThan = hoursAndMinsToSecs(pendingMoreThan); pendingDuration.moreThan = hoursAndMinsToSecs(moreState);
from = hoursAndMinsToSecs(pendingFrom); pendingDuration.from = hoursAndMinsToSecs(fromState);
to = hoursAndMinsToSecs(pendingTo); pendingDuration.to = hoursAndMinsToSecs(toState);
dispatch("set-filter", { lessThan, moreThan, from, to }); setFilter({duration: pendingDuration});
}} }}
> >
Close & Apply Close & Apply
</Button> </Button>
<Button <Button
color="warning" color="warning"
on:click={() => { onclick={() => {
lessThan = null; resetStates();
moreThan = null;
from = null;
to = null;
reset();
}}>Reset Values</Button }}>Reset Values</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
lessThan = null; resetStates();
moreThan = null; resetPending();
from = null; setFilter({duration: pendingDuration});
to = null;
reset();
dispatch("set-filter", { lessThan, moreThan, from, to });
}}>Reset Filter</Button }}>Reset Filter</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
+24 -25
View File
@@ -10,7 +10,6 @@
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
Button, Button,
Modal, Modal,
@@ -20,49 +19,49 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
isOpen = $bindable(false),
presetEnergy= {from: null, to: null},
setFilter,
} = $props();
const energyMaximum = 1000.0; /* State Init */
let energyState = $state(presetEnergy);
export let isOpen = false;
export let energy= {from: null, to: null};
function resetRanges() {
energy.from = null
energy.to = null
}
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Filter based on energy</ModalHeader> <ModalHeader>Filter based on energy</ModalHeader>
<ModalBody> <ModalBody>
<h4>Total Job Energy (kWh)</h4> <div class="mb-3">
<DoubleRangeSlider <div class="mb-0"><b>Total Job Energy (kWh)</b></div>
on:change={({ detail }) => ( <DoubleRangeSlider
(energy.from = detail[0]), (energy.to = detail[1]) changeRange={(detail) => {
)} energyState.from = detail[0];
min={0.0} energyState.to = detail[1];
max={energyMaximum} }}
firstSlider={energy?.from ? energy.from : 0.0} sliderMin={0.0}
secondSlider={energy?.to ? energy.to : energyMaximum} sliderMax={1000.0}
inputFieldFrom={energy?.from ? energy.from : null} fromPreset={energyState?.from? energyState.from : 0.0}
inputFieldTo={energy?.to ? energy.to : null} toPreset={energyState?.to? energyState.to : 1000.0}
/> />
</div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { on:click={() => {
isOpen = false; isOpen = false;
dispatch("set-filter", { energy }); setFilter({ energy: energyState });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { on:click={() => {
isOpen = false; isOpen = false;
resetRanges(); energyState = {from: null, to: null};
dispatch("set-filter", { energy }); setFilter({ energy: energyState });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>
@@ -4,16 +4,22 @@
Properties: Properties:
- `icon String`: Sveltestrap icon name - `icon String`: Sveltestrap icon name
- `modified Bool?`: Optional if filter is modified [Default: false] - `modified Bool?`: Optional if filter is modified [Default: false]
- `onclick Fn()`: Opens Modal on click
- `children Fn()?`: Internal prop, Svelte 5 version of <slot/>
--> -->
<script> <script>
import { Button, Icon } from "@sveltestrap/sveltestrap"; import { Button, Icon } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */
export let icon; let { icon, modified, onclick, children } = $props();
export let modified = false;
</script> </script>
<Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} on:click> <Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} {onclick}>
<Icon name={icon} /> <Icon name={icon} />
<slot /> {#if children}
<!-- Note: Ignore '@' Error in IDE -->
{@render children()}
{:else}
<span>No content found</span>
{/if}
</Button> </Button>
@@ -13,7 +13,7 @@
- `const allJobStates [String]`: List of all available job states used in cc-backend - `const allJobStates [String]`: List of all available job states used in cc-backend
--> -->
<script context="module"> <script module>
export const allJobStates = [ export const allJobStates = [
"running", "running",
"completed", "completed",
@@ -27,7 +27,8 @@
</script> </script>
<script> <script>
import { createEventDispatcher } from "svelte"; /* Note: Ignore VSCode reported 'A component can only have one instance-level <script> element' error */
import { import {
Button, Button,
ListGroup, ListGroup,
@@ -38,16 +39,16 @@
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
isOpen = $bindable(false),
presetStates = [...allJobStates],
setFilter
} = $props();
export let isModified = false; /* State Init */
export let isOpen = false; let pendingStates = $state([...presetStates]);
export let states = [...allJobStates];
let pendingStates = [...states];
$: isModified =
states.length != pendingStates.length ||
!states.every((state) => pendingStates.includes(state));
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@@ -71,28 +72,25 @@
<Button <Button
color="primary" color="primary"
disabled={pendingStates.length == 0} disabled={pendingStates.length == 0}
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
states = [...pendingStates]; setFilter({ states: [...pendingStates] });
dispatch("set-filter", { states });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="warning" color="warning"
on:click={() => { onclick={() => {
states = [...allJobStates];
pendingStates = []; pendingStates = [];
}}>Deselect All</Button }}>Deselect All</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
states = [...allJobStates];
pendingStates = [...allJobStates]; pendingStates = [...allJobStates];
dispatch("set-filter", { states }); setFilter({ states: [...pendingStates] });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
+164 -154
View File
@@ -2,14 +2,11 @@
@component Filter sub-component for selecting job resources @component Filter sub-component for selecting job resources
Properties: Properties:
- `cluster Object?`: The currently selected cluster config [Default: null]
- `isOpen Bool?`: Is this filter component opened [Default: false] - `isOpen Bool?`: Is this filter component opened [Default: false]
- `activeCluster String?`: The currently selected cluster name [Default: null]
- `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}] - `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}]
- `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}] - `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}]
- `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}] - `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}]
- `isNodesModified Bool?`: Is the node filter modified [Default: false]
- `isHwthreadsModified Bool?`: Is the Hwthreads filter modified [Default: false]
- `isAccsModified Bool?`: Is the Accelerator filter modified [Default: false]
- `namedNode String?`: The currently selected single named node (= hostname) [Default: null] - `namedNode String?`: The currently selected single named node (= hostname) [Default: null]
Events: Events:
@@ -17,7 +14,7 @@
--> -->
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { getContext } from "svelte";
import { import {
Button, Button,
Modal, Modal,
@@ -28,27 +25,19 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
const clusters = getContext("clusters"), /* Svelte 5 Props*/
initialized = getContext("initialized"), let {
dispatch = createEventDispatcher(); isOpen = $bindable(false),
activeCluster = null,
export let cluster = null; presetNumNodes = { from: null, to: null },
export let isOpen = false; presetNumHWThreads = { from: null, to: null },
export let numNodes = { from: null, to: null }; presetNumAccelerators = { from: null, to: null },
export let numHWThreads = { from: null, to: null }; presetNamedNode = null,
export let numAccelerators = { from: null, to: null }; presetNodeMatch = "eq",
export let isNodesModified = false; setFilter
export let isHwthreadsModified = false; } = $props()
export let isAccsModified = false;
export let namedNode = null;
export let nodeMatch = "eq"
let pendingNumNodes = numNodes,
pendingNumHWThreads = numHWThreads,
pendingNumAccelerators = numAccelerators,
pendingNamedNode = namedNode,
pendingNodeMatch = nodeMatch;
/* Const Init */
const nodeMatchLabels = { const nodeMatchLabels = {
eq: "Equal To", eq: "Equal To",
contains: "Contains", contains: "Contains",
@@ -85,75 +74,133 @@
0, 0,
); );
let minNumNodes = 1, /* State Init*/
maxNumNodes = 0, // Counts
minNumHWThreads = 1, let minNumNodes = $state(1);
maxNumHWThreads = 0, let maxNumNodes = $state(0);
minNumAccelerators = 0, let maxNumHWThreads = $state(0);
maxNumAccelerators = 0; let maxNumAccelerators = $state(0);
$: { // Pending
let pendingNumNodes = $state(presetNumNodes);
let pendingNumHWThreads = $state(presetNumHWThreads);
let pendingNumAccelerators = $state(presetNumAccelerators);
let pendingNamedNode = $state(presetNamedNode);
let pendingNodeMatch = $state(presetNodeMatch);
// Changable States
let nodesState = $state(presetNumNodes);
let threadState = $state(presetNumHWThreads);
let accState = $state(presetNumAccelerators);
/* Derived States */
const clusters = $derived(getContext("clusters"));
const initialized = $derived(getContext("initialized"));
// Is Selection Active
const nodesActive = $derived(!(JSON.stringify(nodesState) === JSON.stringify({ from: 1, to: maxNumNodes })));
const threadActive = $derived(!(JSON.stringify(threadState) === JSON.stringify({ from: 1, to: maxNumHWThreads })));
const accActive = $derived(!(JSON.stringify(accState) === JSON.stringify({ from: 0, to: maxNumAccelerators })));
// Block Apply if null
const disableApply = $derived(
nodesState.from === null || nodesState.to === null ||
threadState.from === null || threadState.to === null ||
accState.from === null || accState.to === null
);
/* Reactive Effects | Svelte 5 onMount */
$effect(() => {
if ($initialized) { if ($initialized) {
if (cluster != null) { // 'hClusters' defined in templates/base.tmpl
const { subClusters } = clusters.find((c) => c.name == cluster); if (activeCluster != null) {
const { filterRanges } = header.clusters.find((c) => c.name == cluster); const { filterRanges } = hClusters.find((c) => c.name == activeCluster);
minNumNodes = filterRanges.numNodes.from; minNumNodes = filterRanges.numNodes.from;
maxNumNodes = filterRanges.numNodes.to; maxNumNodes = filterRanges.numNodes.to;
} else if (clusters.length > 0) {
for (let hc of hClusters) {
const { filterRanges } = hc;
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
};
};
};
});
$effect(() => {
if ($initialized) {
// 'hClusters' defined in templates/base.tmpl
if (activeCluster != null) {
const { subClusters } = clusters.find((c) => c.name == activeCluster);
maxNumAccelerators = findMaxNumAccels([{ subClusters }]); maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]); maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]);
} else if (clusters.length > 0) { } else if (clusters.length > 0) {
const { filterRanges } = header.clusters[0];
minNumNodes = filterRanges.numNodes.from;
maxNumNodes = filterRanges.numNodes.to;
maxNumAccelerators = findMaxNumAccels(clusters); maxNumAccelerators = findMaxNumAccels(clusters);
maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters); maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters);
for (let cluster of header.clusters) {
const { filterRanges } = cluster;
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
}
} }
} }
} });
$: { $effect(() => {
if ( if (
isOpen &&
$initialized && $initialized &&
pendingNumNodes.from == null && pendingNumNodes.from == null &&
pendingNumNodes.to == null pendingNumNodes.to == null
) { ) {
pendingNumNodes = { from: 0, to: maxNumNodes }; nodesState = { from: 1, to: maxNumNodes };
} }
} });
$: { $effect(() => {
if ( if (
isOpen &&
$initialized && $initialized &&
((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) || pendingNumHWThreads.from == null &&
isHwthreadsModified == false) pendingNumHWThreads.to == null
) { ) {
pendingNumHWThreads = { from: 0, to: maxNumHWThreads }; threadState = { from: 1, to: maxNumHWThreads };
} }
} });
$: if (maxNumAccelerators != null && maxNumAccelerators > 1) { $effect(() => {
if ( if (
isOpen &&
$initialized && $initialized &&
pendingNumAccelerators.from == null && pendingNumAccelerators.from == null &&
pendingNumAccelerators.to == null pendingNumAccelerators.to == null
) { ) {
pendingNumAccelerators = { from: 0, to: maxNumAccelerators }; accState = { from: 0, to: maxNumAccelerators };
} }
} });
/* Functions */
function setResources() {
if (nodesActive) {
pendingNumNodes = {...nodesState};
} else {
pendingNumNodes = { from: null, to: null };
};
if (threadActive) {
pendingNumHWThreads = {...threadState};
} else {
pendingNumHWThreads = { from: null, to: null };
};
if (accActive) {
pendingNumAccelerators = {...accState};
} else {
pendingNumAccelerators = { from: null, to: null };
};
};
function resetResources() {
pendingNumNodes = { from: null, to: null };
pendingNumHWThreads = { from: null, to: null };
pendingNumAccelerators = { from: null, to: null };
pendingNamedNode = null;
pendingNodeMatch = "eq";
};
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select number of utilized Resources</ModalHeader> <ModalHeader>Select number of utilized Resources</ModalHeader>
<ModalBody> <ModalBody>
<h6>Named Node</h6> <div><b>Named Node</b></div>
<div class="d-flex"> <div class="d-flex mb-3">
<Input type="text" class="w-75" bind:value={pendingNamedNode} /> <Input type="text" class="w-75" bind:value={pendingNamedNode} />
<div class="mx-1"></div> <div class="mx-1"></div>
<Input type="select" class="w-25" bind:value={pendingNodeMatch}> <Input type="select" class="w-25" bind:value={pendingNodeMatch}>
@@ -164,82 +211,63 @@
{/each} {/each}
</Input> </Input>
</div> </div>
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
<DoubleRangeSlider <div class="mb-3">
on:change={({ detail }) => { <div class="mb-0"><b>Number of Nodes</b></div>
pendingNumNodes = { from: detail[0], to: detail[1] };
isNodesModified = true;
}}
min={minNumNodes}
max={maxNumNodes}
firstSlider={pendingNumNodes.from}
secondSlider={pendingNumNodes.to}
inputFieldFrom={pendingNumNodes.from}
inputFieldTo={pendingNumNodes.to}
/>
<h6 style="margin-top: 1rem;">
Number of HWThreads (Use for Single-Node Jobs)
</h6>
<DoubleRangeSlider
on:change={({ detail }) => {
pendingNumHWThreads = { from: detail[0], to: detail[1] };
isHwthreadsModified = true;
}}
min={minNumHWThreads}
max={maxNumHWThreads}
firstSlider={pendingNumHWThreads.from}
secondSlider={pendingNumHWThreads.to}
inputFieldFrom={pendingNumHWThreads.from}
inputFieldTo={pendingNumHWThreads.to}
/>
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
<DoubleRangeSlider <DoubleRangeSlider
on:change={({ detail }) => { changeRange={(detail) => {
pendingNumAccelerators = { from: detail[0], to: detail[1] }; nodesState.from = detail[0];
isAccsModified = true; nodesState.to = detail[1];
}} }}
min={minNumAccelerators} sliderMin={minNumNodes}
max={maxNumAccelerators} sliderMax={maxNumNodes}
firstSlider={pendingNumAccelerators.from} fromPreset={nodesState.from}
secondSlider={pendingNumAccelerators.to} toPreset={nodesState.to}
inputFieldFrom={pendingNumAccelerators.from}
inputFieldTo={pendingNumAccelerators.to}
/> />
</div>
<div class="mb-3">
<div class="mb-0"><b>Number of HWThreads</b> (Use for Single-Node Jobs)</div>
<DoubleRangeSlider
changeRange={(detail) => {
threadState.from = detail[0];
threadState.to = detail[1];
}}
sliderMin={1}
sliderMax={maxNumHWThreads}
fromPreset={threadState.from}
toPreset={threadState.to}
/>
</div>
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
<div>
<div class="mb-0"><b>Number of Accelerators</b></div>
<DoubleRangeSlider
changeRange={(detail) => {
accState.from = detail[0];
accState.to = detail[1];
}}
sliderMin={0}
sliderMax={maxNumAccelerators}
fromPreset={accState.from}
toPreset={accState.to}
/>
</div>
{/if} {/if}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null} disabled={disableApply}
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
pendingNumNodes = isNodesModified setResources();
? pendingNumNodes setFilter({
: { from: null, to: null }; numNodes: pendingNumNodes,
pendingNumHWThreads = isHwthreadsModified numHWThreads: pendingNumHWThreads,
? pendingNumHWThreads numAccelerators: pendingNumAccelerators,
: { from: null, to: null }; node: pendingNamedNode,
pendingNumAccelerators = isAccsModified nodeMatch: pendingNodeMatch
? pendingNumAccelerators
: { from: null, to: null };
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
numHWThreads = {
from: pendingNumHWThreads.from,
to: pendingNumHWThreads.to,
};
numAccelerators = {
from: pendingNumAccelerators.from,
to: pendingNumAccelerators.to,
};
namedNode = pendingNamedNode;
nodeMatch = pendingNodeMatch;
dispatch("set-filter", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
nodeMatch
}); });
}} }}
> >
@@ -247,36 +275,18 @@
</Button> </Button>
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
pendingNumNodes = { from: null, to: null }; resetResources();
pendingNumHWThreads = { from: null, to: null }; setFilter({
pendingNumAccelerators = { from: null, to: null }; numNodes: pendingNumNodes,
pendingNamedNode = null; numHWThreads: pendingNumHWThreads,
pendingNodeMatch = null; numAccelerators: pendingNumAccelerators,
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }; node: pendingNamedNode,
numHWThreads = { nodeMatch: pendingNodeMatch
from: pendingNumHWThreads.from,
to: pendingNumHWThreads.to,
};
numAccelerators = {
from: pendingNumAccelerators.from,
to: pendingNumAccelerators.to,
};
isNodesModified = false;
isHwthreadsModified = false;
isAccsModified = false;
namedNode = pendingNamedNode;
nodeMatch = pendingNodeMatch;
dispatch("set-filter", {
numNodes,
numHWThreads,
numAccelerators,
namedNode,
nodeMatch
}); });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
@@ -12,8 +12,19 @@
- `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component - `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component
--> -->
<script module>
export const startTimeSelectOptions = [
{ range: "", rangeLabel: "No Selection"},
{ range: "last6h", rangeLabel: "Last 6hrs"},
{ range: "last24h", rangeLabel: "Last 24hrs"},
{ range: "last7d", rangeLabel: "Last 7 days"},
{ range: "last30d", rangeLabel: "Last 30 days"}
];
</script>
<script> <script>
import { createEventDispatcher } from "svelte"; /* Note: Ignore VSCode reported 'A component can only have one instance-level <script> element' error */
import { parse, format, sub } from "date-fns"; import { parse, format, sub } from "date-fns";
import { import {
Row, Row,
@@ -26,44 +37,39 @@
FormGroup, FormGroup,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
export let isModified = false; isOpen = $bindable(false),
export let isOpen = false; presetStartTime = { from: null, to: null, range: "" },
export let from = null; setFilter
export let to = null; } = $props();
export let range = "";
export let startTimeSelectOptions;
/* Const Init */
const now = new Date(Date.now()); const now = new Date(Date.now());
const ago = sub(now, { months: 1 }); const ago = sub(now, { months: 1 });
const defaultFrom = { const resetFrom = { date: format(ago, "yyyy-MM-dd"), time: format(ago, "HH:mm")};
date: format(ago, "yyyy-MM-dd"), const resetTo = { date: format(now, "yyyy-MM-dd"), time: format(now, "HH:mm")};
time: format(ago, "HH:mm"),
};
const defaultTo = {
date: format(now, "yyyy-MM-dd"),
time: format(now, "HH:mm"),
};
$: pendingFrom = (from == null) ? defaultFrom : fromRFC3339(from) /* State Init */
$: pendingTo = (to == null) ? defaultTo : fromRFC3339(to) let pendingStartTime = $state(presetStartTime);
$: pendingRange = range let fromState = $state(fromRFC3339(presetStartTime?.from, resetFrom));
let toState = $state(fromRFC3339(presetStartTime?.to, resetTo));
$: isModified = /* Derived Init*/
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) && const rangeSelect = $derived(pendingStartTime?.range ? pendingStartTime.range : "")
(range != pendingRange) &&
!( /* Functions */
from == null && function fromRFC3339(rfc3339, reset) {
pendingFrom.date == "0000-00-00" && if (rfc3339) {
pendingFrom.time == "00:00" const parsedDate = new Date(rfc3339);
) && return {
!( date: format(parsedDate, "yyyy-MM-dd"),
to == null && time: format(parsedDate, "HH:mm"),
pendingTo.date == "0000-00-00" && }
pendingTo.time == "00:00" } else {
) && return reset
!( range == "" && pendingRange == ""); }
}
function toRFC3339({ date, time }, secs = "00") { function toRFC3339({ date, time }, secs = "00") {
const parsedDate = parse( const parsedDate = parse(
@@ -73,26 +79,18 @@
); );
return parsedDate.toISOString(); return parsedDate.toISOString();
} }
function fromRFC3339(rfc3339) {
const parsedDate = new Date(rfc3339);
return {
date: format(parsedDate, "yyyy-MM-dd"),
time: format(parsedDate, "HH:mm"),
};
}
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Select Start Time</ModalHeader> <ModalHeader>Select Start Time</ModalHeader>
<ModalBody> <ModalBody>
{#if range !== ""} {#if rangeSelect !== ""}
<h4>Current Range</h4> <h4>Current Range</h4>
<Row> <Row>
<FormGroup class="col"> <FormGroup class="col">
<Input type ="select" bind:value={pendingRange} > <Input type ="select" bind:value={pendingStartTime.range} >
{#each startTimeSelectOptions as { rangeLabel, range }} {#each startTimeSelectOptions as { rangeLabel, range }}
<option label={rangeLabel} value={range}/> <option label={rangeLabel} value={range}></option>
{/each} {/each}
</Input> </Input>
</FormGroup> </FormGroup>
@@ -101,42 +99,41 @@
<h4>From</h4> <h4>From</h4>
<Row> <Row>
<FormGroup class="col"> <FormGroup class="col">
<Input type="date" bind:value={pendingFrom.date} disabled={pendingRange !== ""}/> <Input type="date" bind:value={fromState.date} disabled={rangeSelect !== ""}/>
</FormGroup> </FormGroup>
<FormGroup class="col"> <FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time} disabled={pendingRange !== ""}/> <Input type="time" bind:value={fromState.time} disabled={rangeSelect !== ""}/>
</FormGroup> </FormGroup>
</Row> </Row>
<h4>To</h4> <h4>To</h4>
<Row> <Row>
<FormGroup class="col"> <FormGroup class="col">
<Input type="date" bind:value={pendingTo.date} disabled={pendingRange !== ""}/> <Input type="date" bind:value={toState.date} disabled={rangeSelect !== ""}/>
</FormGroup> </FormGroup>
<FormGroup class="col"> <FormGroup class="col">
<Input type="time" bind:value={pendingTo.time} disabled={pendingRange !== ""}/> <Input type="time" bind:value={toState.time} disabled={rangeSelect !== ""}/>
</FormGroup> </FormGroup>
</Row> </Row>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
{#if pendingRange !== ""} {#if rangeSelect !== ""}
<Button <Button
color="warning" color="warning"
disabled={pendingRange === ""} disabled={rangeSelect === ""}
on:click={() => { onclick={() => {
pendingRange = "" pendingStartTime.range = "";
}} }}
> >
Reset Range Reset Range
</Button> </Button>
<Button <Button
color="primary" color="primary"
disabled={pendingRange === ""} disabled={rangeSelect === ""}
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
from = null; pendingStartTime.from = null;
to = null; pendingStartTime.to = null;
range = pendingRange; setFilter({ startTime: pendingStartTime });
dispatch("set-filter", { from, to, range });
}} }}
> >
Close & Apply Range Close & Apply Range
@@ -144,14 +141,14 @@
{:else} {:else}
<Button <Button
color="primary" color="primary"
disabled={pendingFrom.date == "0000-00-00" || disabled={fromState.date == "0000-00-00" ||
pendingTo.date == "0000-00-00"} toState.date == "0000-00-00"}
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
from = toRFC3339(pendingFrom); pendingStartTime.from = toRFC3339(fromState);
to = toRFC3339(pendingTo, "59"); pendingStartTime.to = toRFC3339(toState, "59");
range = ""; pendingStartTime.range = "";
dispatch("set-filter", { from, to, range }); setFilter({ startTime: pendingStartTime });
}} }}
> >
Close & Apply Dates Close & Apply Dates
@@ -159,14 +156,16 @@
{/if} {/if}
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
from = null; fromState = resetFrom;
to = null; toState = resetTo;
range = ""; pendingStartTime.from = null;
dispatch("set-filter", { from, to, range }); pendingStartTime.to = null;
pendingStartTime.range = "";
setFilter({ startTime: pendingStartTime });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
+41 -50
View File
@@ -2,7 +2,6 @@
@component Filter sub-component for selecting job statistics @component Filter sub-component for selecting job statistics
Properties: Properties:
- `isModified Bool?`: Is this filter component modified [Default: false]
- `isOpen Bool?`: Is this filter component opened [Default: false] - `isOpen Bool?`: Is this filter component opened [Default: false]
- `stats [Object]?`: The currently selected statistics filter [Default: []] - `stats [Object]?`: The currently selected statistics filter [Default: []]
@@ -11,7 +10,6 @@
--> -->
<script> <script>
import { createEventDispatcher, getContext } from "svelte";
import { getStatsItems } from "../utils.js"; import { getStatsItems } from "../utils.js";
import { import {
Button, Button,
@@ -22,75 +20,68 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte"; import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
const initialized = getContext("initialized"), /* Svelte 5 Props */
dispatch = createEventDispatcher(); let {
isOpen = $bindable(),
presetStats,
setFilter
} = $props();
export let isModified = false; /* Derived Init */
export let isOpen = false; const availableStats = $derived(getStatsItems(presetStats));
export let stats = [];
let statistics = [];
function loadRanges(isInitialized) {
if (!isInitialized) return;
statistics = getStatsItems(stats);
}
/* Functions */
function resetRanges() { function resetRanges() {
for (let st of statistics) { for (let as of availableStats) {
st.enabled = false as.enabled = false
st.from = 0 as.from = 0
st.to = st.peak as.to = as.peak
} };
} }
$: isModified = !statistics.every((a) => {
let b = stats.find((s) => s.field == a.field);
if (b == null) return !a.enabled;
return a.from == b.from && a.to == b.to;
});
$: loadRanges($initialized);
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>Filter based on statistics</ModalHeader> <ModalHeader>
<span>Filter based on statistics</span>
</ModalHeader>
<ModalBody> <ModalBody>
{#each statistics as stat} {#each availableStats as aStat}
<h4>{stat.text}</h4> <div class="mb-3">
<DoubleRangeSlider <div class="mb-0"><b>{aStat.text}</b></div>
on:change={({ detail }) => ( <DoubleRangeSlider
(stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true) changeRange={(detail) => {
)} aStat.from = detail[0];
min={0} aStat.to = detail[1];
max={stat.peak} if (aStat.from == 0 && aStat.to == aStat.peak) {
firstSlider={stat.from} aStat.enabled = false;
secondSlider={stat.to} } else {
inputFieldFrom={stat.from} aStat.enabled = true;
inputFieldTo={stat.to} }
/> }}
sliderMin={0.0}
sliderMax={aStat.peak}
fromPreset={aStat.from}
toPreset={aStat.to}
/>
</div>
{/each} {/each}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
stats = statistics.filter((stat) => stat.enabled); setFilter({ stats: [...availableStats.filter((as) => as.enabled)] });
dispatch("set-filter", { stats });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="danger" color="danger"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
resetRanges(); resetRanges();
stats = []; setFilter({stats: []});
dispatch("set-filter", { stats });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
+28 -24
View File
@@ -11,7 +11,7 @@
--> -->
<script> <script>
import { createEventDispatcher, getContext } from "svelte"; import { getContext } from "svelte";
import { import {
Button, Button,
ListGroup, ListGroup,
@@ -26,20 +26,20 @@
import { fuzzySearchTags } from "../utils.js"; import { fuzzySearchTags } from "../utils.js";
import Tag from "../helper/Tag.svelte"; import Tag from "../helper/Tag.svelte";
const allTags = getContext("tags"), /* Svelte 5 Props */
initialized = getContext("initialized"), let {
dispatch = createEventDispatcher(); isOpen = $bindable(false),
presetTags = [],
setFilter
} = $props();
export let isModified = false; /* Derived */
export let isOpen = false; const allTags = $derived(getContext("tags"))
export let tags = []; const initialized = $derived(getContext("initialized"))
let pendingTags = [...tags]; /* State Init */
$: isModified = let pendingTags = $state(presetTags);
tags.length != pendingTags.length || let searchTerm = $state("");
!tags.every((tagId) => pendingTags.includes(tagId));
let searchTerm = "";
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@@ -55,7 +55,7 @@
<Button <Button
outline outline
color="danger" color="danger"
on:click={() => onclick={() =>
(pendingTags = pendingTags.filter((id) => id != tag.id))} (pendingTags = pendingTags.filter((id) => id != tag.id))}
> >
<Icon name="dash-circle" /> <Icon name="dash-circle" />
@@ -64,7 +64,7 @@
<Button <Button
outline outline
color="success" color="success"
on:click={() => (pendingTags = [...pendingTags, tag.id])} onclick={() => (pendingTags = [...pendingTags, tag.id])}
> >
<Icon name="plus-circle" /> <Icon name="plus-circle" />
</Button> </Button>
@@ -81,21 +81,25 @@
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="primary"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
tags = [...pendingTags]; setFilter({ tags: [...pendingTags] });
dispatch("set-filter", { tags });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
<Button <Button
color="danger" color="warning"
on:click={() => { onclick={() => {
isOpen = false;
tags = [];
pendingTags = []; pendingTags = [];
dispatch("set-filter", { tags }); }}>Clear Selection</Button
>
<Button
color="danger"
onclick={() => {
isOpen = false;
pendingTags = [];
setFilter({ tags: [...pendingTags] });
}}>Reset</Button }}>Reset</Button
> >
<Button on:click={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
@@ -17,11 +17,14 @@
Icon Icon
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let cJobs; /* Svelte 5 Props */
export let showLinks = false; let {
export let renderCard = false; cJobs,
export let width = "auto"; showLinks = false,
export let height = "400px"; renderCard = false,
width = "auto",
height = "400px",
} = $props();
</script> </script>
{#if renderCard} {#if renderCard}
@@ -23,79 +23,90 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { findJobFootprintThresholds } from "../utils.js"; import { findJobFootprintThresholds } from "../utils.js";
export let job; /* Svelte 5 Props */
export let displayTitle = true; let {
export let width = "auto"; job,
export let height = "310px"; displayTitle = true,
width = "auto",
height = "310px",
} = $props();
const footprintData = job?.footprint?.map((jf) => { /* Derived */
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name); const footprintData = $derived(buildFootprint(job?.footprint));
if (fmc) {
// Unit /* Functions */
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "") function buildFootprint(input) {
let result = input?.map((jf) => {
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
if (fmc) {
// Unit
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
// Threshold / -Differences // Threshold / -Differences
const fmt = findJobFootprintThresholds(job, jf.stat, fmc); const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
// Define basic data -> Value: Use as Provided // Define basic data -> Value: Use as Provided
const fmBase = { const fmBase = {
name: jf.name + ' (' + jf.stat + ')', name: jf.name + ' (' + jf.stat + ')',
avg: jf.value, avg: jf.value,
unit: unit, unit: unit,
max: fmt.peak, max: fmt.peak,
dir: fmc.lowerIsBetter dir: fmc.lowerIsBetter
};
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
return {
...fmBase,
color: "danger",
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 3
}; };
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
return {
...fmBase,
color: "danger",
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 3
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
return {
...fmBase,
color: "warning",
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Footprint value within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
return {
...fmBase,
color: "info",
message:
"Footprint value above expected normal threshold: Check for artifacts recommended.",
impact: 0,
};
} else {
return {
...fmBase,
color: "secondary",
message:
"Footprint value above expected peak threshold: Check for artifacts!",
impact: -1,
};
}
} else { // No matching metric config: display as single value
return { return {
...fmBase, name: jf.name + ' (' + jf.stat + ')',
color: "warning", avg: jf.value,
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Footprint value within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
return {
...fmBase,
color: "info",
message: message:
"Footprint value above expected normal threshold: Check for artifacts recommended.", `No config for metric ${jf.name} found.`,
impact: 0, impact: 4,
};
} else {
return {
...fmBase,
color: "secondary",
message:
"Footprint value above expected peak threshold: Check for artifacts!",
impact: -1,
}; };
} }
} else { // No matching metric config: display as single value }).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
return { return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
name: jf.name + ' (' + jf.stat + ')', });;
avg: jf.value,
message: return result;
`No config for metric ${jf.name} found.`, };
impact: 4,
};
}
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
});;
function evalFootprint(value, thresholds, lowerIsBetter, level) { function evalFootprint(value, thresholds, lowerIsBetter, level) {
// Handle Metrics in which less value is better // Handle Metrics in which less value is better
@@ -176,7 +187,7 @@
>{fpd.message}</Tooltip >{fpd.message}</Tooltip
> >
</div> </div>
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}"> <Row cols={12} class={(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}>
{#if fpd.dir} {#if fpd.dir}
<Col xs="1"> <Col xs="1">
<Icon name="caret-left-fill" /> <Icon name="caret-left-fill" />
@@ -8,35 +8,40 @@
- `refresh`: When fired, the upstream component refreshes its contents - `refresh`: When fired, the upstream component refreshes its contents
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap"; import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap";
const dispatch = createEventDispatcher(); /* Svelte 5 Props */
let {
initially = null,
presetClass = "",
onRefresh
} = $props();
let refreshInterval = null; /* State Init */
let refreshInterval = $state(initially ? initially * 1000 : null);
/* Var Init */
let refreshIntervalId = null; let refreshIntervalId = null;
/* Functions */
function refreshIntervalChanged() { function refreshIntervalChanged() {
if (refreshIntervalId != null) clearInterval(refreshIntervalId); if (refreshIntervalId != null) clearInterval(refreshIntervalId);
if (refreshInterval == null) return; if (refreshInterval == null) return;
refreshIntervalId = setInterval(() => onRefresh(), refreshInterval);
refreshIntervalId = setInterval(() => dispatch("refresh"), refreshInterval);
} }
export let initially = null; /* Svelte 5 onMount */
$effect(() => {
if (initially != null) {
refreshInterval = initially * 1000;
refreshIntervalChanged(); refreshIntervalChanged();
} });
</script> </script>
<InputGroup> <InputGroup class={presetClass}>
<Input <Input
type="select" type="select"
title="Periodic refresh interval" title="Periodic refresh interval"
bind:value={refreshInterval} bind:value={refreshInterval}
on:change={refreshIntervalChanged} onchange={refreshIntervalChanged}
> >
<option value={null}>No Interval</option> <option value={null}>No Interval</option>
<option value={30 * 1000}>30 Seconds</option> <option value={30 * 1000}>30 Seconds</option>
@@ -46,7 +51,7 @@
</Input> </Input>
<Button <Button
outline outline
on:click={() => dispatch("refresh")} onclick={() => onRefresh()}
disabled={refreshInterval != null} disabled={refreshInterval != null}
> >
<Icon name="arrow-clockwise" /> Refresh <Icon name="arrow-clockwise" /> Refresh
+18 -9
View File
@@ -9,21 +9,30 @@
<script> <script>
import { getContext } from 'svelte' import { getContext } from 'svelte'
const allTags = getContext('tags'),
initialized = getContext('initialized') /* Svelte 5 Props */
let {
id = null,
tag = null,
clickable = true
} = $props();
export let id = null /* Derived */
export let tag = null const allTags = $derived(getContext('tags'));
export let clickable = true const initialized = $derived(getContext('initialized'));
if (tag != null && id == null) /* Effects */
id = tag.id $effect(() => {
if (tag != null && id == null)
id = tag.id
});
$: { $effect(() => {
if ($initialized && tag == null) if ($initialized && tag == null)
tag = allTags.find(tag => tag.id == id) tag = allTags.find(tag => tag.id == id)
} });
/* Function*/
function getScopeColor(scope) { function getScopeColor(scope) {
switch (scope) { switch (scope) {
case "admin": case "admin":
@@ -33,26 +33,42 @@
import { fuzzySearchTags } from "../utils.js"; import { fuzzySearchTags } from "../utils.js";
import Tag from "./Tag.svelte"; import Tag from "./Tag.svelte";
export let job; /* Svelte 5 Props */
export let jobTags = job.tags; let {
export let username; jobTags = $bindable(),
export let authlevel; job,
export let roles; username,
export let renderModal = true; authlevel,
roles,
renderModal = true,
} = $props();
let allTags = getContext("tags"), /* Const Init */
initialized = getContext("initialized");
let newTagType = "",
newTagName = "",
newTagScope = username;
let filterTerm = "";
let pendingChange = false;
let isOpen = false;
const isAdmin = (roles && authlevel == roles.admin); const isAdmin = (roles && authlevel == roles.admin);
const isSupport = (roles && authlevel == roles.support); const isSupport = (roles && authlevel == roles.support);
const client = getContextClient(); const client = getContextClient();
/* State Init */
let initialized = getContext("initialized")
let allTags = getContext("tags")
let newTagType = $state("");
let newTagName = $state("");
let newTagScope = $state(username);
let filterTerm = $state("");
let pendingChange = $state(false);
let isOpen = $state(false);
/* Derived Init */
const allTagsFiltered = $derived(($initialized, jobTags, fuzzySearchTags(filterTerm, allTags))); // $init und JobTags only for triggering react
const usedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'used', isAdmin, isSupport));
const unusedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'unused', isAdmin, isSupport));
/* Effects */
$effect(() => {
updateNewTag(filterTerm)
});
/* Const Mutations */
const createTagMutation = ({ type, name, scope }) => { const createTagMutation = ({ type, name, scope }) => {
return mutationStore({ return mutationStore({
client: client, client: client,
@@ -104,19 +120,16 @@
}); });
}; };
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags)); /* Functions */
$: usedTagsFiltered = matchJobTags(jobTags, allTagsFiltered, 'used', isAdmin, isSupport); function updateNewTag(term) {
$: unusedTagsFiltered = matchJobTags(jobTags, allTagsFiltered, 'unused', isAdmin, isSupport);
$: {
newTagType = ""; newTagType = "";
newTagName = ""; newTagName = "";
let parts = filterTerm.split(":").map((s) => s.trim()); let parts = term.split(":").map((s) => s.trim());
if (parts.length == 2 && parts.every((s) => s.length > 0)) { if (parts.length == 2 && parts.every((s) => s.length > 0)) {
newTagType = parts[0]; newTagType = parts[0];
newTagName = parts[1]; newTagName = parts[1];
} };
} };
function matchJobTags(tags, availableTags, type, isAdmin, isSupport) { function matchJobTags(tags, availableTags, type, isAdmin, isSupport) {
const jobTagIds = tags.map((t) => t.id) const jobTagIds = tags.map((t) => t.id)
@@ -183,7 +196,7 @@
</script> </script>
{#if renderModal} {#if renderModal}
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => {isOpen = !isOpen; filterTerm = "";}}>
<ModalHeader> <ModalHeader>
Manage Tags <Icon name="tags"/> Manage Tags <Icon name="tags"/>
</ModalHeader> </ModalHeader>
@@ -232,7 +245,7 @@
<Button <Button
size="sm" size="sm"
color="danger" color="danger"
on:click={() => removeTagFromJob(utag)} onclick={() => removeTagFromJob(utag)}
> >
<Icon name="x" /> <Icon name="x" />
</Button> </Button>
@@ -282,7 +295,7 @@
<Button <Button
size="sm" size="sm"
color="success" color="success"
on:click={() => addTagToJob(uutag)} onclick={() => addTagToJob(uutag)}
> >
<Icon name="plus" /> <Icon name="plus" />
</Button> </Button>
@@ -314,7 +327,7 @@
outline outline
style="width:100%;" style="width:100%;"
color="success" color="success"
on:click={(e) => ( onclick={(e) => (
e.preventDefault(), createTag(newTagType, newTagName, newTagScope) e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
)} )}
> >
@@ -345,11 +358,11 @@
{/if} {/if}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button> <Button color="primary" onclick={() => {isOpen = false; filterTerm = "";}}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
<Button outline on:click={() => (isOpen = true)} size="sm" color="primary"> <Button outline onclick={() => (isOpen = true)} size="sm" color="primary">
Manage {jobTags?.length ? jobTags.length : ''} Tags Manage {jobTags?.length ? jobTags.length : ''} Tags
</Button> </Button>
@@ -387,7 +400,7 @@
<Button <Button
size="sm" size="sm"
color="danger" color="danger"
on:click={() => removeTagFromJob(utag)} onclick={() => removeTagFromJob(utag)}
> >
<Icon name="x" /> <Icon name="x" />
</Button> </Button>
@@ -396,7 +409,7 @@
<Button <Button
size="sm" size="sm"
color="danger" color="danger"
on:click={() => removeTagFromJob(utag)} onclick={() => removeTagFromJob(utag)}
> >
<Icon name="x" /> <Icon name="x" />
</Button> </Button>
@@ -435,7 +448,7 @@
<Button <Button
size="sm" size="sm"
color="success" color="success"
on:click={() => addTagToJob(uutag)} onclick={() => addTagToJob(uutag)}
> >
<Icon name="plus" /> <Icon name="plus" />
</Button> </Button>
@@ -444,7 +457,7 @@
<Button <Button
size="sm" size="sm"
color="success" color="success"
on:click={() => addTagToJob(uutag)} onclick={() => addTagToJob(uutag)}
> >
<Icon name="plus" /> <Icon name="plus" />
</Button> </Button>
@@ -475,7 +488,7 @@
outline outline
style="width:100%;" style="width:100%;"
color="success" color="success"
on:click={(e) => ( onclick={(e) => (
e.preventDefault(), createTag(newTagType, newTagName, newTagScope) e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
)} )}
> >
@@ -12,21 +12,31 @@
<script> <script>
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap"; import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
import { createEventDispatcher } from "svelte";
import { scramble, scrambleNames } from "../utils.js"; import { scramble, scrambleNames } from "../utils.js";
const dispatch = createEventDispatcher(); // Note: If page with this component has project preset, keep preset until reset
/* Svelte 5 Props */
let {
presetProject = "",
authlevel = null,
roles = null,
setFilter
} = $props();
export let presetProject = ""; // If page with this component has project preset, keep preset until reset /* Const Init*/
export let authlevel = null; const throttle = 500;
export let roles = null;
let mode = presetProject ? "jobName" : "project"; /* Var Init */
let term = "";
let user = ""; let user = "";
let project = presetProject ? presetProject : ""; let project = presetProject ? presetProject : "";
let jobName = ""; let jobName = "";
const throttle = 500; let timeoutId = null;
/* State Init */
let mode = $state(presetProject ? "jobName" : "project");
let term = $state("");
/* Functions */
function modeChanged() { function modeChanged() {
if (mode == "user") { if (mode == "user") {
project = presetProject ? presetProject : ""; project = presetProject ? presetProject : "";
@@ -41,7 +51,6 @@
termChanged(0); termChanged(0);
} }
let timeoutId = null;
// Compatibility: Handle "user role" and "no role" identically // Compatibility: Handle "user role" and "no role" identically
function termChanged(sleep = throttle) { function termChanged(sleep = throttle) {
if (roles && authlevel >= roles.manager) { if (roles && authlevel >= roles.manager) {
@@ -52,7 +61,7 @@
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
dispatch("set-filter", { setFilter({
user, user,
project, project,
jobName jobName
@@ -65,7 +74,7 @@
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
dispatch("set-filter", { setFilter({
project, project,
jobName jobName
}); });
@@ -78,6 +87,8 @@
term = "" term = ""
presetProject = "" presetProject = ""
project = "" project = ""
jobName = ""
user = ""
termChanged(0); termChanged(0);
} }
</script> </script>
@@ -86,10 +97,10 @@
<Input <Input
type="select" type="select"
style="max-width: 120px;" style="max-width: 120px;"
class="form-select" class="form-select w-auto"
title="Search Mode" title="Search Mode"
bind:value={mode} bind:value={mode}
on:change={modeChanged} onchange={modeChanged}
> >
{#if !presetProject} {#if !presetProject}
<option value={"project"}>Project</option> <option value={"project"}>Project</option>
@@ -102,12 +113,12 @@
<Input <Input
type="text" type="text"
bind:value={term} bind:value={term}
on:change={() => termChanged()} onchange={() => termChanged()}
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)} onkeyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
placeholder={presetProject ? `Find ${mode} in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Find ${mode} ...`} placeholder={presetProject ? `Find in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Find ${mode} ...`}
/> />
{#if presetProject} {#if presetProject}
<Button title="Reset Project" on:click={resetProject} <Button title="Reset Project" onclick={() => resetProject()}
><Icon name="arrow-counterclockwise" /></Button ><Icon name="arrow-counterclockwise" /></Button
> >
{/if} {/if}
+29 -29
View File
@@ -12,15 +12,22 @@
import Tag from "../helper/Tag.svelte"; import Tag from "../helper/Tag.svelte";
import TagManagement from "../helper/TagManagement.svelte"; import TagManagement from "../helper/TagManagement.svelte";
export let job; /* Svelte 5 Props */
export let jobTags = job.tags; let {
export let showTagedit = false; job,
export let username = null; jobTags = job.tags,
export let authlevel= null; showTagedit = false,
export let roles = null; username = null,
export let isSelected = null; authlevel= null,
export let showSelect = false; roles = null,
isSelected = null,
showSelect = false,
} = $props();
/* State Init */
let displayCheck = $state(false);
/* Functions */
function formatDuration(duration) { function formatDuration(duration) {
const hours = Math.floor(duration / 3600); const hours = Math.floor(duration / 3600);
duration -= hours * 3600; duration -= hours * 3600;
@@ -41,9 +48,8 @@
} }
} }
let displayCheck = false;
function clipJobId(jid) { function clipJobId(jid) {
displayCheck = true;
// Navigator clipboard api needs a secure context (https) // Navigator clipboard api needs a secure context (https)
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard navigator.clipboard
@@ -65,14 +71,11 @@
textArea.remove(); textArea.remove();
} }
} }
setTimeout(function () {
displayCheck = false;
}, 1000);
} }
</script> </script>
<div> <div>
<p class="mb-2"> <p class="mb-2 text-truncate">
<span class="d-flex justify-content-between"> <span class="d-flex justify-content-between">
<span class="align-self-center fw-bold mr-2"> <span class="align-self-center fw-bold mr-2">
<a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a> <a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
@@ -81,7 +84,7 @@
<span> <span>
{#if showSelect} {#if showSelect}
<Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2" <Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2"
on:click={() => { onclick={() => {
isSelected = !isSelected isSelected = !isSelected
}}> }}>
{#if isSelected} {#if isSelected}
@@ -98,7 +101,13 @@
{ 'Add or Remove Job to/from Comparison Selection' } { 'Add or Remove Job to/from Comparison Selection' }
</Tooltip> </Tooltip>
{/if} {/if}
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" on:click={clipJobId(job.jobId)} > <Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" onclick={() => {
displayCheck = true;
clipJobId(job.jobId);
setTimeout(function () {
displayCheck = false;
}, 1000);
}}>
{#if displayCheck} {#if displayCheck}
<Icon name="clipboard2-check-fill"/> <Icon name="clipboard2-check-fill"/>
{:else} {:else}
@@ -113,16 +122,15 @@
</span> </span>
</span> </span>
{#if job.metaData?.jobName} {#if job.metaData?.jobName}
{#if job.metaData?.jobName.length <= 25} {#if job.metaData?.jobName.length <= 20}
<div>{job.metaData.jobName}</div> <span>{job.metaData.jobName}</span>
{:else} {:else}
<div <span
class="truncate"
style="cursor:help;" style="cursor:help;"
title={job.metaData.jobName} title={job.metaData.jobName}
> >
{job.metaData.jobName} {job.metaData.jobName}
</div> </span>
{/if} {/if}
{/if} {/if}
{#if job.arrayJobId} {#if job.arrayJobId}
@@ -207,11 +215,3 @@
</p> </p>
{/if} {/if}
</div> </div>
<style>
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -11,11 +11,51 @@
- Dispatched once immediately and then each time page or itemsPerPage changes - Dispatched once immediately and then each time page or itemsPerPage changes
--> -->
<script>
/* Svelte 5 Props */
let {
page = 1,
itemsPerPage = 10,
totalItems = 0,
itemText = "items",
pageSizes = [10,25,50],
updatePaging
} = $props();
/* Derived */
const backButtonDisabled = $derived((page === 1));
const nextButtonDisabled = $derived((page >= (totalItems / itemsPerPage)));
/* Functions */
function pageUp ( event ) {
event.preventDefault();
page += 1;
updatePaging({ itemsPerPage, page });
}
function pageBack ( event ) {
event.preventDefault();
page -= 1;
updatePaging({ itemsPerPage, page });
}
function pageReset ( event ) {
event.preventDefault();
page = 1;
updatePaging({ itemsPerPage, page });
}
function updateItems ( event ) {
event.preventDefault();
updatePaging({ itemsPerPage, page });
}
</script>
<div class="cc-pagination" > <div class="cc-pagination" >
<div class="cc-pagination-left"> <div class="cc-pagination-left">
<label for="cc-pagination-select">{ itemText } per page:</label> <label for="cc-pagination-select">{ itemText } per page:</label>
<div class="cc-pagination-select-wrapper"> <div class="cc-pagination-select-wrapper">
<select on:blur|preventDefault={reset} bind:value={itemsPerPage} id="cc-pagination-select" class="cc-pagination-select"> <select onblur={(e) => pageReset(e)} onchange={(e) => updateItems(e)} bind:value={itemsPerPage} id="cc-pagination-select" class="cc-pagination-select">
{#each pageSizes as size} {#each pageSizes as size}
<option value="{size}">{size}</option> <option value="{size}">{size}</option>
{/each} {/each}
@@ -23,54 +63,23 @@
<span class="focus"></span> <span class="focus"></span>
</div> </div>
<span class="cc-pagination-text"> <span class="cc-pagination-text">
{ (page - 1) * itemsPerPage } - { Math.min((page - 1) * itemsPerPage + itemsPerPage, totalItems) } of { totalItems } { itemText } { ((page - 1) * itemsPerPage) + 1 } - { Math.min((page - 1) * itemsPerPage + itemsPerPage, totalItems) } of { totalItems } { itemText }
</span> </span>
</div> </div>
<div class="cc-pagination-right"> <div class="cc-pagination-right">
{#if !backButtonDisabled} {#if !backButtonDisabled}
<button class="reset nav" type="button" <button aria-label="page-reset" class="reset nav" type="button"
on:click|preventDefault="{reset}"></button> onclick={(e) => pageReset(e)}></button>
<button class="left nav" type="button" <button aria-label="page-back" class="left nav" type="button"
on:click|preventDefault="{() => { page -= 1; }}"></button> onclick={(e) => pageBack(e)}></button>
{/if} {/if}
{#if !nextButtonDisabled} {#if !nextButtonDisabled}
<button class="right nav" type="button" <button aria-label="page-up" class="right nav" type="button"
on:click|preventDefault="{() => { page += 1; }}"></button> onclick={(e) => pageUp(e)}></button>
{/if} {/if}
</div> </div>
</div> </div>
<script>
import { createEventDispatcher } from "svelte";
export let page = 1;
export let itemsPerPage = 10;
export let totalItems = 0;
export let itemText = "items";
export let pageSizes = [10,25,50];
let backButtonDisabled, nextButtonDisabled;
const dispatch = createEventDispatcher();
$: {
if (typeof page !== "number") {
page = Number(page);
}
if (typeof itemsPerPage !== "number") {
itemsPerPage = Number(itemsPerPage);
}
dispatch("update-paging", { itemsPerPage, page });
}
$: backButtonDisabled = (page === 1);
$: nextButtonDisabled = (page >= (totalItems / itemsPerPage));
function reset ( event ) {
page = 1;
}
</script>
<style> <style>
*, *::before, *::after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
@@ -18,86 +18,30 @@
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onMount, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card } from "@sveltestrap/sveltestrap";
export let metric = "";
export let width = 0;
export let height = 300;
export let data = null;
export let xlabel = "";
export let xticks = [];
export let xinfo = [];
export let ylabel = "";
export let yunit = "";
export let title = "";
export let forResources = false;
export let plotSync;
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed // NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
/* Svelte 5 Props */
let {
metric = "",
width = 0,
height = 300,
data = null,
xlabel = "",
xticks = [],
xinfo = [],
ylabel = "",
yunit = "",
title = "",
forResources = false,
plotSync,
} = $props();
/* Const Init */
const clusterCockpitConfig = getContext("cc-config"); const clusterCockpitConfig = getContext("cc-config");
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio; const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false; const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
// UPLOT PLUGIN // converts the legend into a simple tooltip // UPLOT SERIES INIT //
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, {
minWidth: "100px",
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style,
});
// hide series color markers:
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
const overEl = u.over;
overEl.style.overflow = "visible";
// move legend into plot bounds
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
}
function update(u) {
const { left, top } = u.cursor;
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
}
return {
hooks: {
init: init,
setCursor: update,
},
};
}
const plotSeries = [ const plotSeries = [
{ {
label: "JobID", label: "JobID",
@@ -122,6 +66,7 @@
}, },
] ]
// UPLOT SCALES INIT //
if (forResources) { if (forResources) {
const resSeries = [ const resSeries = [
{ {
@@ -177,11 +122,13 @@
plotSeries.push(...statsSeries) plotSeries.push(...statsSeries)
}; };
// UPLOT BAND COLORS //
const plotBands = [ const plotBands = [
{ series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" }, { series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
{ series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" }, { series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
]; ];
// UPLOT OPTIONS //
const opts = { const opts = {
width, width,
height, height,
@@ -259,11 +206,83 @@
} }
}; };
// RENDER HANDLING /* Var Init */
let plotWrapper = null;
let uplot = null;
let timeoutId = null; let timeoutId = null;
let uplot = null;
/* State Init */
let plotWrapper = $state(null);
/* Effects */
$effect(() => {
if (plotWrapper) {
onSizeChange(width, height);
}
});
/* Functions */
// UPLOT PLUGIN // converts the legend into a simple tooltip
function legendAsTooltipPlugin({
className,
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
} = {}) {
let legendEl;
function init(u, opts) {
legendEl = u.root.querySelector(".u-legend");
legendEl.classList.remove("u-inline");
className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, {
minWidth: "100px",
textAlign: "left",
pointerEvents: "none",
display: "none",
position: "absolute",
left: 0,
top: 0,
zIndex: 100,
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
...style,
});
// hide series color markers:
const idents = legendEl.querySelectorAll(".u-marker");
for (let i = 0; i < idents.length; i++)
idents[i].style.display = "none";
const overEl = u.over;
overEl.style.overflow = "visible";
// move legend into plot bounds
overEl.appendChild(legendEl);
// show/hide tooltip on enter/exit
overEl.addEventListener("mouseenter", () => {
legendEl.style.display = null;
});
overEl.addEventListener("mouseleave", () => {
legendEl.style.display = "none";
});
}
function update(u) {
const { left, top } = u.cursor;
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
}
return {
hooks: {
init: init,
setCursor: update,
},
};
}
// RENDER HANDLING
function render(ren_width, ren_height) { function render(ren_width, ren_height) {
if (!uplot) { if (!uplot) {
opts.width = ren_width; opts.width = ren_width;
@@ -284,29 +303,25 @@
}, 200); }, 200);
} }
/* On Mount */
onMount(() => { onMount(() => {
if (plotWrapper) { if (plotWrapper) {
render(width, height); render(width, height);
} }
}); });
/* On Destroy */
onDestroy(() => { onDestroy(() => {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
if (uplot) uplot.destroy(); if (uplot) uplot.destroy();
}); });
// This updates plot on all size changes if wrapper (== data) exists
$: if (plotWrapper) {
onSizeChange(width, height);
}
</script> </script>
<!-- Define $width Wrapper and NoData Card --> <!-- Define $width Wrapper and NoData Card -->
{#if data && data[0].length > 0} {#if data && data[0].length > 0}
<div bind:this={plotWrapper} bind:clientWidth={width} <div bind:this={plotWrapper} bind:clientWidth={width}
style="background-color: rgba(255, 255, 255, 1.0);" class="rounded" style="background-color: rgba(255, 255, 255, 1.0);" class="rounded"
/> ></div>
{:else} {:else}
<Card body color="warning" class="mx-4 my-2" <Card body color="warning" class="mx-4 my-2"
>Cannot render plot: No series data returned for <code>{metric?metric:'job resources'}</code></Card >Cannot render plot: No series data returned for <code>{metric?metric:'job resources'}</code></Card
@@ -244,7 +244,7 @@
<!-- Define Wrapper and NoData Card within $width --> <!-- Define Wrapper and NoData Card within $width -->
<div bind:clientWidth={width}> <div bind:clientWidth={width}>
{#if data.length > 0} {#if data.length > 0}
<div bind:this={plotWrapper} /> <div bind:this={plotWrapper}></div>
{:else} {:else}
<Card class="mx-4" body color="warning" <Card class="mx-4" body color="warning"
>Cannot render histogram: No data!</Card >Cannot render histogram: No data!</Card
@@ -591,7 +591,7 @@
{#if series[0]?.data && series[0].data.length > 0} {#if series[0]?.data && series[0].data.length > 0}
<div bind:this={plotWrapper} bind:clientWidth={width} <div bind:this={plotWrapper} bind:clientWidth={width}
style="background-color: {backgroundColor()};" class={forNode ? 'py-2 rounded' : 'rounded'} style="background-color: {backgroundColor()};" class={forNode ? 'py-2 rounded' : 'rounded'}
/> ></div>
{:else} {:else}
<Card body color="warning" class="mx-4" <Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{metric}</code></Card >Cannot render plot: No series data returned for <code>{metric}</code></Card
+18 -22
View File
@@ -1,5 +1,5 @@
<!-- <!--
@component Pie Plot based on uPlot Pie @component Pie Plot based on chart.js Pie
Properties: Properties:
- `size Number`: X and Y size of the plot, for square shape - `size Number`: X and Y size of the plot, for square shape
@@ -31,33 +31,17 @@
] ]
</script> </script>
<script> <script>
import { Pie } from 'svelte-chartjs'; import Chart from 'chart.js/auto'
import { import { onMount } from 'svelte';
Chart as ChartJS,
Title,
Tooltip,
Legend,
Filler,
ArcElement,
CategoryScale
} from 'chart.js';
ChartJS.register(
Title,
Tooltip,
Legend,
Filler,
ArcElement,
CategoryScale
);
export let canvasId = "pie-default"
export let size export let size
export let sliceLabel export let sliceLabel
export let quantities export let quantities
export let entities export let entities
export let displayLegend = false export let displayLegend = false
$: data = { const data = {
labels: entities, labels: entities,
datasets: [ datasets: [
{ {
@@ -79,10 +63,22 @@
} }
} }
onMount(() => {
new Chart(
document.getElementById(canvasId),
{
type: 'pie',
data: data,
options: options
}
);
});
</script> </script>
<!-- <div style="width: 500px;"><canvas id="dimensions"></canvas></div><br/> -->
<div class="chart-container" style="--container-width: {size}; --container-height: {size}"> <div class="chart-container" style="--container-width: {size}; --container-height: {size}">
<Pie {data} {options}/> <canvas id={canvasId}></canvas>
</div> </div>
<style> <style>
+19 -3
View File
@@ -1,5 +1,5 @@
<!-- <!--
@component Polar Plot based on chartJS Radar @component Polar Plot based on chart.js Radar
Properties: Properties:
- `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ] - `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ]
@@ -8,7 +8,8 @@
--> -->
<script> <script>
import { Radar } from 'svelte-chartjs'; import { getContext, onMount } from 'svelte'
import Chart from 'chart.js/auto'
import { import {
Chart as ChartJS, Chart as ChartJS,
Title, Title,
@@ -30,8 +31,10 @@
LineElement LineElement
); );
export let polarMetrics = []; export let polarMetrics = [];
export let polarData = []; export let polarData = [];
export let canvasId = "polar-default";
export let height = 350; export let height = 350;
const labels = polarMetrics const labels = polarMetrics
@@ -113,10 +116,23 @@
} }
} }
onMount(() => {
new Chart(
document.getElementById(canvasId),
{
type: 'radar',
data: data,
options: options,
height: height
}
);
});
</script> </script>
<!-- <div style="width: 500px;"><canvas id="dimensions"></canvas></div><br/> -->
<div class="chart-container"> <div class="chart-container">
<Radar {data} {options} {height}/> <canvas id={canvasId}></canvas>
</div> </div>
<style> <style>
@@ -363,7 +363,7 @@
</script> </script>
{#if data != null} {#if data != null}
<div bind:this={plotWrapper} class="p-2"/> <div bind:this={plotWrapper} class="p-2"></div>
{:else} {:else}
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card <Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
> >
@@ -2,6 +2,7 @@
Copyright (c) 2021 Michael Keller Copyright (c) 2021 Michael Keller
Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider) Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider)
Changes: remove dependency, text inputs, configurable value ranges, on:change event Changes: remove dependency, text inputs, configurable value ranges, on:change event
Changes #2: Rewritten for Svelte 5, removed bodyHandler
--> -->
<!-- <!--
@component Selector component to display range selections via min and max double-sliders @component Selector component to display range selections via min and max double-sliders
@@ -9,82 +10,80 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
Properties: Properties:
- min: Number - min: Number
- max: Number - max: Number
- firstSlider: Number (Starting position of slider #1) - sliderHandleFrom: Number (Starting position of slider #1)
- secondSlider: Number (Starting position of slider #2) - sliderHandleTo: Number (Starting position of slider #2)
Events: Events:
- `change`: [Number, Number] (Positions of the two sliders) - `change`: [Number, Number] (Positions of the two sliders)
--> -->
<script> <script>
import { createEventDispatcher } from "svelte"; let {
sliderMin,
sliderMax,
fromPreset = 1,
toPreset = 100,
changeRange
} = $props();
export let min; let pendingValues = $state([fromPreset, toPreset]);
export let max; let sliderFrom = $state(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.));
export let firstSlider; let sliderTo = $state(Math.min(((toPreset == null ? sliderMin : toPreset) - sliderMin) / (sliderMax - sliderMin), 1.));
export let secondSlider; let inputFieldFrom = $state(fromPreset.toString());
export let inputFieldFrom = 0; let inputFieldTo = $state(toPreset.toString());
export let inputFieldTo = 0; let leftHandle = $state();
let sliderMain = $state();
const dispatch = createEventDispatcher();
let values;
let start, end; /* Positions of sliders from 0 to 1 */
$: values = [firstSlider, secondSlider]; /* Avoid feedback loop */
$: start = Math.max(((firstSlider == null ? min : firstSlider) - min) / (max - min), 0);
$: end = Math.min(((secondSlider == null ? min : secondSlider) - min) / (max - min), 1);
let leftHandle;
let body;
let slider;
let timeoutId = null; let timeoutId = null;
function queueChangeEvent() { function queueChangeEvent() {
if (timeoutId !== null) { if (timeoutId !== null) {
clearTimeout(timeoutId); clearTimeout(timeoutId)
} }
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null; timeoutId = null
changeRange(pendingValues);
// Show selection but avoid feedback loop }, 100);
if (values[0] != null && inputFieldFrom != values[0].toString())
inputFieldFrom = values[0].toString();
if (values[1] != null && inputFieldTo != values[1].toString())
inputFieldTo = values[1].toString();
dispatch('change', values);
}, 250);
} }
function update() { function updateStates(newValue, newPosition, target) {
values = [ if (target === 'from') {
Math.floor(min + start * (max - min)), pendingValues[0] = isNaN(newValue) ? null : newValue;
Math.floor(min + end * (max - min)) inputFieldFrom = isNaN(newValue) ? null : newValue.toString();
]; sliderFrom = newPosition;
queueChangeEvent(); } else if (target === 'to') {
} pendingValues[1] = isNaN(newValue) ? null : newValue;
inputFieldTo = isNaN(newValue) ? null : newValue.toString();
function inputChanged(idx, event) { sliderTo = newPosition;
let val = Number.parseInt(event.target.value);
if (Number.isNaN(val) || val < min) {
event.target.classList.add('bad');
return;
} }
values[idx] = val;
event.target.classList.remove('bad');
if (idx == 0)
start = clamp((val - min) / (max - min), 0., 1.);
else
end = clamp((val - min) / (max - min), 0., 1.);
queueChangeEvent(); queueChangeEvent();
} }
function clamp(x, min, max) { function rangeChanged (evt, target) {
return x < min evt.preventDefault()
? min evt.stopPropagation()
: (x < max ? x : max); const { left, right } = sliderMain.getBoundingClientRect();
const parentWidth = right - left;
const newP = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
const newV = Math.floor(sliderMin + newP * (sliderMax - sliderMin));
updateStates(newV, newP, target);
}
function inputChanged(evt, target) {
evt.preventDefault()
evt.stopPropagation()
const newV = Number.parseInt(evt.target.value);
const newP = clamp((newV - sliderMin) / (sliderMax - sliderMin), 0., 1.)
updateStates(newV, newP, target);
}
function clamp(x, testMin, testMax) {
return x < testMin
? testMin
: (x > testMax
? testMax
: x
);
} }
function draggable(node) { function draggable(node) {
@@ -151,84 +150,38 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
}; };
} }
function setHandlePosition (which) {
return function (evt) {
const { left, right } = slider.getBoundingClientRect();
const parentWidth = right - left;
const p = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
if (which === 'start') {
start = p;
end = Math.max(end, p);
} else {
start = Math.min(p, start);
end = p;
}
update();
}
}
function setHandlesFromBody (evt) {
const { width } = body.getBoundingClientRect();
const { left, right } = slider.getBoundingClientRect();
const parentWidth = right - left;
const leftHandleLeft = leftHandle.getBoundingClientRect().left;
const pxStart = clamp((leftHandleLeft + evt.detail.dx) - left, 0, parentWidth - width);
const pxEnd = clamp(pxStart + width, width, parentWidth);
const pStart = pxStart / parentWidth;
const pEnd = pxEnd / parentWidth;
start = pStart;
end = pEnd;
update();
}
</script> </script>
<div class="double-range-container"> <div class="double-range-container">
<div class="header"> <div class="header">
<input class="form-control" type="text" placeholder="from..." bind:value={inputFieldFrom} <input class="form-control" type="text" placeholder="from..." value={inputFieldFrom}
on:input={(e) => inputChanged(0, e)} /> oninput={(e) => inputChanged(e, 'from')} />
<span>Full Range: <b> {min} </b> - <b> {max} </b></span> <span>Full Range: <b> {sliderMin} </b> - <b> {sliderMax} </b></span>
<input class="form-control" type="text" placeholder="to..." bind:value={inputFieldTo} <input class="form-control" type="text" placeholder="to..." value={inputFieldTo}
on:input={(e) => inputChanged(1, e)} /> oninput={(e) => inputChanged(e, 'to')} />
</div> </div>
<div class="slider" bind:this={slider}>
<div id="slider-active" class="slider" bind:this={sliderMain}>
<div <div
class="body" class="slider-body"
bind:this={body} style="left: {100 * sliderFrom}%;right: {100 * (1 - sliderTo)}%;"
use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlesFromBody}"
style="
left: {100 * start}%;
right: {100 * (1 - end)}%;
"
></div> ></div>
<div <div
class="handle" class="slider-handle"
bind:this={leftHandle} bind:this={leftHandle}
data-which="start" data-which="from"
use:draggable use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('start')}" ondragmove={(e) => rangeChanged(e, 'from')}
style=" style="left: {100 * sliderFrom}%"
left: {100 * start}%
"
></div> ></div>
<div <div
class="handle" class="slider-handle"
data-which="end" data-which="to"
use:draggable use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('end')}" ondragmove={(e) => rangeChanged(e, 'to')}
style=" style="left: {100 * sliderTo}%"
left: {100 * end}%
"
></div> ></div>
</div> </div>
</div> </div>
@@ -238,7 +191,8 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: -5px; align-items: flex-end;
margin-bottom: 5px;
} }
.header :nth-child(2) { .header :nth-child(2) {
padding-top: 10px; padding-top: 10px;
@@ -249,17 +203,13 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
width: 100px; width: 100px;
} }
:global(.double-range-container .header input[type="text"].bad) {
color: #ff5c33;
border-color: #ff5c33;
}
.double-range-container { .double-range-container {
width: 100%; width: 100%;
height: 50px; height: 50px;
user-select: none; user-select: none;
box-sizing: border-box; box-sizing: border-box;
white-space: nowrap white-space: nowrap;
margin-top: -4px;
} }
.slider { .slider {
position: relative; position: relative;
@@ -271,13 +221,13 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c; box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c;
border-radius: 6px; border-radius: 6px;
} }
.handle { .slider-handle {
position: absolute; position: absolute;
top: 50%; top: 50%;
width: 0; width: 0;
height: 0; height: 0;
} }
.handle:after { .slider-handle:after {
content: ' '; content: ' ';
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
@@ -291,11 +241,11 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
/* .handle[data-which="end"]:after{ /* .handle[data-which="end"]:after{
transform: translate(-100%, -50%); transform: translate(-100%, -50%);
} */ } */
.handle:active:after { .slider-handle:active:after {
background-color: #ddd; background-color: #ddd;
z-index: 9; z-index: 9;
} }
.body { .slider-body {
top: 0; top: 0;
position: absolute; position: absolute;
background-color: #34a1ff; background-color: #34a1ff;
@@ -20,16 +20,24 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let cluster; /* Svelte 5 Props */
export let selectedHistograms; let {
export let isOpen; cluster,
isOpen = $bindable(),
presetSelectedHistograms,
applyChange
} = $props();
/* Const Init */
const client = getContextClient(); const client = getContextClient();
const initialized = getContext("initialized");
function loadHistoMetrics(isInitialized, thisCluster) { /* Derived */
if (!isInitialized) return []; let selectedHistograms = $derived(presetSelectedHistograms); // Non-Const Derived: Is settable
const availableMetrics = $derived(loadHistoMetrics(cluster));
/* Functions */
function loadHistoMetrics(thisCluster) {
// isInit Check Removed: Parent Component has finished Init-Query: Globalmetrics available here.
if (!thisCluster) { if (!thisCluster) {
return getContext("globalMetrics") return getContext("globalMetrics")
.filter((gm) => gm?.footprint) .filter((gm) => gm?.footprint)
@@ -42,18 +50,6 @@
} }
} }
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
function updateConfiguration(data) { function updateConfiguration(data) {
updateConfigurationMutation({ updateConfigurationMutation({
name: data.name, name: data.name,
@@ -67,6 +63,7 @@
function closeAndApply() { function closeAndApply() {
isOpen = !isOpen; isOpen = !isOpen;
applyChange(selectedHistograms)
updateConfiguration({ updateConfiguration({
name: cluster name: cluster
? `user_view_histogramMetrics:${cluster}` ? `user_view_histogramMetrics:${cluster}`
@@ -75,8 +72,18 @@
}); });
} }
$: availableMetrics = loadHistoMetrics($initialized, cluster); /* Mutation */
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
</script> </script>
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}> <Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
@@ -92,7 +99,7 @@
</ListGroup> </ListGroup>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button> <Button color="primary" on:click={() => closeAndApply()}>Close & Apply</Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button> <Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
@@ -12,7 +12,7 @@
--> -->
<script> <script>
import { getContext, createEventDispatcher } from "svelte"; import { getContext } from "svelte";
import { import {
Modal, Modal,
ModalBody, ModalBody,
@@ -23,57 +23,23 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { gql, getContextClient, mutationStore } from "@urql/svelte"; import { gql, getContextClient, mutationStore } from "@urql/svelte";
export let metrics; /* Svelte 5 Props */
export let isOpen; let {
export let configName; isOpen = $bindable(false),
export let allMetrics = null; showFootprint = $bindable(false),
export let cluster = null; totalMetrics = $bindable(0),
export let subCluster = null; presetMetrics = [],
export let showFootprint = false; cluster = null,
export let footprintSelect = false; subCluster = null,
footprintSelect = false,
const onInit = getContext("on-init") preInitialized = false, // Job View is Pre-Init'd: $initialized "alone" store returns false
const globalMetrics = getContext("globalMetrics") configName,
const dispatch = createEventDispatcher(); applyMetrics
} = $props();
let newMetricsOrder = [];
let unorderedMetrics = [...metrics];
let pendingShowFootprint = !!showFootprint;
onInit(() => {
if (allMetrics == null) allMetrics = new Set();
for (let metric of globalMetrics) allMetrics.add(metric.name);
});
$: {
if (allMetrics != null) {
if (!cluster) {
for (let metric of globalMetrics) allMetrics.add(metric.name);
} else {
allMetrics.clear();
for (let gm of globalMetrics) {
if (!subCluster) {
if (gm.availability.find((av) => av.cluster === cluster)) allMetrics.add(gm.name);
} else {
if (gm.availability.find((av) => av.cluster === cluster && av.subClusters.includes(subCluster))) allMetrics.add(gm.name);
}
}
}
newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m));
newMetricsOrder.unshift(...metrics.filter((m) => allMetrics.has(m)));
unorderedMetrics = unorderedMetrics.filter((m) => allMetrics.has(m));
}
}
function printAvailability(metric, cluster) {
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
if (!cluster) {
return avail.map((av) => av.cluster).join(',')
} else {
return avail.find((av) => av.cluster === cluster).subClusters.join(',')
}
}
/* Const Init */
const globalMetrics = getContext("globalMetrics");
const initialized = getContext("initialized");
const client = getContextClient(); const client = getContextClient();
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@@ -87,7 +53,51 @@
}); });
}; };
let columnHovering = null; /* State Init */
let pendingMetrics = $state(presetMetrics);
let pendingShowFootprint = $state(!!showFootprint);
let listedMetrics = $state([]);
let columnHovering = $state(null);
/* Derives States */
const allMetrics = $derived(loadAvailable(preInitialized || $initialized));
/* Reactive Effects */
$effect(() => {
totalMetrics = allMetrics.size;
});
$effect(() => {
listedMetrics = [...presetMetrics, ...allMetrics.difference(new Set(presetMetrics))]; // List (preset) active metrics first, then list inactives
});
/* Functions */
function loadAvailable(init) {
const availableMetrics = new Set();
if (init) {
for (let gm of globalMetrics) {
if (!cluster) {
availableMetrics.add(gm.name)
} else {
if (!subCluster) {
if (gm.availability.find((av) => av.cluster === cluster)) availableMetrics.add(gm.name);
} else {
if (gm.availability.find((av) => av.cluster === cluster && av.subClusters.includes(subCluster))) availableMetrics.add(gm.name);
}
}
}
}
return availableMetrics
}
function printAvailability(metric, cluster) {
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
if (!cluster) {
return avail.map((av) => av.cluster).join(',')
} else {
return avail.find((av) => av.cluster === cluster).subClusters.join(',')
}
}
function columnsDragStart(event, i) { function columnsDragStart(event, i) {
event.dataTransfer.effectAllowed = "move"; event.dataTransfer.effectAllowed = "move";
@@ -98,18 +108,21 @@
function columnsDrag(event, target) { function columnsDrag(event, target) {
event.dataTransfer.dropEffect = "move"; event.dataTransfer.dropEffect = "move";
const start = Number.parseInt(event.dataTransfer.getData("text/plain")); const start = Number.parseInt(event.dataTransfer.getData("text/plain"));
let pendingMetricsOrder = [...listedMetrics];
if (start < target) { if (start < target) {
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start]); pendingMetricsOrder.splice(target + 1, 0, listedMetrics[start]);
newMetricsOrder.splice(start, 1); pendingMetricsOrder.splice(start, 1);
} else { } else {
newMetricsOrder.splice(target, 0, newMetricsOrder[start]); pendingMetricsOrder.splice(target, 0, listedMetrics[start]);
newMetricsOrder.splice(start + 1, 1); pendingMetricsOrder.splice(start + 1, 1);
} }
listedMetrics = [...pendingMetricsOrder];
columnHovering = null; columnHovering = null;
} }
function closeAndApply() { function closeAndApply() {
metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m)); pendingMetrics = listedMetrics.filter((m) => pendingMetrics.includes(m));
isOpen = false; isOpen = false;
let configKey; let configKey;
@@ -123,7 +136,7 @@
updateConfigurationMutation({ updateConfigurationMutation({
name: configKey, name: configKey,
value: JSON.stringify(metrics), value: JSON.stringify(pendingMetrics),
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && res.error) { if (res.fetching === false && res.error) {
throw res.error; throw res.error;
@@ -145,7 +158,7 @@
}); });
}; };
dispatch('update-metrics', metrics); applyMetrics(pendingMetrics);
} }
</script> </script>
@@ -159,27 +172,35 @@
</li> </li>
<hr /> <hr />
{/if} {/if}
{#each newMetricsOrder as metric, index (metric)} {#each listedMetrics as metric, index (metric)}
<li <li
class="cc-config-column list-group-item" class="cc-config-column list-group-item"
draggable={true} draggable={true}
ondragover="return false" ondragover={(event) => {
on:dragstart={(event) => columnsDragStart(event, index)} event.preventDefault()
on:drop|preventDefault={(event) => columnsDrag(event, index)} return false
on:dragenter={() => (columnHovering = index)} }}
ondragstart={(event) => {
columnsDragStart(event, index)
}}
ondrop={(event) => {
event.preventDefault()
columnsDrag(event, index)
}}
ondragenter={() => (columnHovering = index)}
class:is-active={columnHovering === index} class:is-active={columnHovering === index}
> >
{#if unorderedMetrics.includes(metric)} {#if pendingMetrics.includes(metric)}
<input <input
type="checkbox" type="checkbox"
bind:group={unorderedMetrics} bind:group={pendingMetrics}
value={metric} value={metric}
checked checked
/> />
{:else} {:else}
<input <input
type="checkbox" type="checkbox"
bind:group={unorderedMetrics} bind:group={pendingMetrics}
value={metric} value={metric}
/> />
{/if} {/if}
@@ -192,8 +213,8 @@
</ListGroup> </ListGroup>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button> <Button color="primary" onclick={() => closeAndApply()}>Close & Apply</Button>
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Cancel</Button> <Button color="secondary" onclick={() => (isOpen = !isOpen)}>Cancel</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
@@ -7,7 +7,7 @@
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext, onMount } from "svelte";
import { import {
Icon, Icon,
Button, Button,
@@ -18,44 +18,73 @@
ModalHeader, ModalHeader,
ModalFooter, ModalFooter,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { getSortItems } from "../utils.js";
export let isOpen = false; /* Svelte 5 Props */
export let sorting = { field: "startTime", type: "col", order: "DESC" }; let {
isOpen = $bindable(false),
let sortableColumns = []; presetSorting = { field: "startTime", type: "col", order: "DESC" },
let activeColumnIdx; applySorting
} = $props();
/* Const Init */
const initialized = getContext("initialized"); const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
function loadSortables(isInitialized) { const fixedSortables = $state([
if (!isInitialized) return; { field: "startTime", type: "col", text: "Start Time (Default)", order: "DESC" },
sortableColumns = [ { field: "duration", type: "col", text: "Duration", order: "DESC" },
{ field: "startTime", type: "col", text: "Start Time", order: "DESC" }, { field: "numNodes", type: "col", text: "Number of Nodes", order: "DESC" },
{ field: "duration", type: "col", text: "Duration", order: "DESC" }, { field: "numHwthreads", type: "col", text: "Number of HWThreads", order: "DESC" },
{ field: "numNodes", type: "col", text: "Number of Nodes", order: "DESC" }, { field: "numAcc", type: "col", text: "Number of Accelerators", order: "DESC" },
{ field: "numHwthreads", type: "col", text: "Number of HWThreads", order: "DESC" }, { field: "energy", type: "col", text: "Total Energy", order: "DESC" },
{ field: "numAcc", type: "col", text: "Number of Accelerators", order: "DESC" }, ]);
{ field: "energy", type: "col", text: "Total Energy", order: "DESC" },
...getSortItems()
]
}
function loadActiveIndex(isInitialized) { /* State Init */
if (!isInitialized) return; let sorting = $state({...presetSorting})
let activeColumnIdx = $state(0);
let metricSortables = $state([]);
/* Derived */
let sortableColumns = $derived([...fixedSortables, ...metricSortables]);
/* Effect */
$effect(() => {
if ($initialized) {
loadMetricSortables();
};
});
/* Functions */
function loadMetricSortables() {
metricSortables = globalMetrics.map((gm) => {
if (gm?.footprint) {
return {
field: gm.name + '_' + gm.footprint,
type: 'foot',
text: gm.name + ' (' + gm.footprint + ')',
order: 'DESC'
}
}
return null
}).filter((r) => r != null)
};
function loadActiveIndex() {
activeColumnIdx = sortableColumns.findIndex( activeColumnIdx = sortableColumns.findIndex(
(col) => col.field == sorting.field, (col) => col.field == sorting.field,
); );
sortableColumns[activeColumnIdx].order = sorting.order; sortableColumns[activeColumnIdx].order = sorting.order;
} }
$: loadSortables($initialized); function resetSorting(sort) {
$: loadActiveIndex($initialized) sorting = {...sort};
loadActiveIndex();
};
</script> </script>
<Modal <Modal
{isOpen} {isOpen}
toggle={() => { toggle={() => {
resetSorting(presetSorting);
isOpen = !isOpen; isOpen = !isOpen;
}} }}
> >
@@ -66,7 +95,7 @@
<ListGroupItem> <ListGroupItem>
<button <button
class="sort" class="sort"
on:click={() => { onclick={() => {
if (activeColumnIdx == i) { if (activeColumnIdx == i) {
col.order = col.order == "DESC" ? "ASC" : "DESC"; col.order = col.order == "DESC" ? "ASC" : "DESC";
} else { } else {
@@ -96,11 +125,27 @@
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button
color="primary" color="warning"
on:click={() => { onclick={() => {
isOpen = false; isOpen = false;
}}>Close</Button resetSorting({ field: "startTime", type: "col", order: "DESC" });
applySorting(sorting);
}}>Reset</Button
> >
<Button
color="primary"
onclick={() => {
applySorting(sorting);
isOpen = false;
}}>Close & Apply</Button
>
<Button
color="secondary"
onclick={() => {
resetSorting(presetSorting);
isOpen = false
}}>Cancel
</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>
@@ -12,7 +12,6 @@
--> -->
<script> <script>
import { createEventDispatcher } from "svelte";
import { import {
Icon, Icon,
Input, Input,
@@ -20,78 +19,96 @@
InputGroupText, InputGroupText,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let from; /* Svelte 5 Props */
export let to; let {
export let customEnabled = true; presetFrom,
export let options = { presetTo,
"Last quarter hour": 15 * 60, customEnabled = true,
"Last half hour": 30 * 60, options = {
"Last hour": 60 * 60, "Last quarter hour": 15 * 60,
"Last 2hrs": 2 * 60 * 60, "Last half hour": 30 * 60,
"Last 4hrs": 4 * 60 * 60, "Last hour": 60 * 60,
"Last 12hrs": 12 * 60 * 60, "Last 2hrs": 2 * 60 * 60,
"Last 24hrs": 24 * 60 * 60, "Last 4hrs": 4 * 60 * 60,
"Last 12hrs": 12 * 60 * 60,
"Last 24hrs": 24 * 60 * 60,
},
applyTime
} = $props();
/* Const Init */
const defaultTo = new Date(Date.now());
const defaultFrom = new Date(defaultTo.setHours(defaultTo.getHours() - 4));
/* State Init */
let timeType = $state("range");
let pendingCustomFrom = $state(null);
let pendingCustomTo = $state(null);
/* Derived */
let timeRange = $derived.by(() => {
if (presetTo && presetFrom) {
return ((presetTo.getTime() - presetFrom.getTime()) / 1000)
} else {
return ((defaultTo.getTime() - defaultFrom.getTime()) / 1000)
}
});
let unknownRange = $derived(!Object.values(options).includes(timeRange));
/* Functions */
function updateTimeRange() {
let now = Date.now();
let t = timeRange * 1000;
applyTime(new Date(now - t), new Date(now));
}; };
$: pendingFrom = from; function updateTimeCustom() {
$: pendingTo = to; if (pendingCustomFrom && pendingCustomTo) {
applyTime(new Date(pendingCustomFrom), new Date(pendingCustomTo));
const dispatch = createEventDispatcher();
let timeRange = // If both times set, return diff, else: display custom select
(to && from) ? ((to.getTime() - from.getTime()) / 1000) : -1;
function updateTimeRange() {
if (timeRange == -1) {
pendingFrom = null;
pendingTo = null;
return;
} }
};
let now = Date.now(),
t = timeRange * 1000;
from = pendingFrom = new Date(now - t);
to = pendingTo = new Date(now);
dispatch("change", { from, to });
}
function updateExplicitTimeRange(type, event) {
let d = new Date(Date.parse(event.target.value));
if (type == "from") pendingFrom = d;
else pendingTo = d;
if (pendingFrom != null && pendingTo != null) {
from = pendingFrom;
to = pendingTo;
dispatch("change", { from, to });
}
}
</script> </script>
<InputGroup class="inline-from"> <InputGroup class="inline-from">
<InputGroupText><Icon name="clock-history" /></InputGroupText> <InputGroupText><Icon name="clock-history" /></InputGroupText>
<InputGroupText>Range</InputGroupText> {#if customEnabled}
<select <Input
class="form-select" type="select"
bind:value={timeRange} style="max-width:fit-content;background-color:#f8f9fa;"
on:change={updateTimeRange} bind:value={timeType}
> >
{#if customEnabled} <option value="range">Range</option>
<option value={-1}>Custom</option> <option value="custom">Custom</option>
{/if} </Input>
{#each Object.entries(options) as [name, seconds]} {:else}
<option value={seconds}>{name}</option> <InputGroupText>Range</InputGroupText>
{/each} {/if}
</select>
{#if timeRange == -1} {#if timeType === "range"}
<Input
type="select"
bind:value={timeRange}
onchange={updateTimeRange}
>
{#if unknownRange}
<option value={timeRange} disabled>Select new range...</option>
{/if}
{#each Object.entries(options) as [name, seconds]}
<option value={seconds}>{name}</option>
{/each}
</Input>
{:else}
<InputGroupText>from</InputGroupText> <InputGroupText>from</InputGroupText>
<Input <Input
type="datetime-local" type="datetime-local"
on:change={(event) => updateExplicitTimeRange("from", event)} bind:value={pendingCustomFrom}
onchange={updateTimeCustom}
></Input> ></Input>
<InputGroupText>to</InputGroupText> <InputGroupText>to</InputGroupText>
<Input <Input
type="datetime-local" type="datetime-local"
on:change={(event) => updateExplicitTimeRange("to", event)} bind:value={pendingCustomTo}
onchange={updateTimeCustom}
></Input> ></Input>
{/if} {/if}
</InputGroup> </InputGroup>
-18
View File
@@ -388,24 +388,6 @@ export function findJobFootprintThresholds(job, stat, metricConfig) {
} }
} }
export function getSortItems() {
//console.time('sort')
const globalMetrics = getContext("globalMetrics")
const result = globalMetrics.map((gm) => {
if (gm?.footprint) {
return {
field: gm.name + '_' + gm.footprint,
type: 'foot',
text: gm.name + ' (' + gm.footprint + ')',
order: 'DESC'
}
}
return null
}).filter((r) => r != null)
//console.timeEnd('sort')
return [...result];
};
function getMetricConfigDeep(metric, cluster, subCluster) { function getMetricConfigDeep(metric, cluster, subCluster) {
const clusters = getContext("clusters"); const clusters = getContext("clusters");
if (cluster != null) { if (cluster != null) {
+12 -5
View File
@@ -1,10 +1,17 @@
import Header from './Header.svelte' import { mount } from 'svelte';
import Header from './Header.svelte';
const headerDomTarget = document.getElementById('svelte-header') const headerDomTarget = document.getElementById('svelte-header');
if (headerDomTarget != null) { if (headerDomTarget != null) {
new Header({ mount(Header, {
target: headerDomTarget, target: headerDomTarget,
props: { ...header }, props: { // { ...header },
}) username: hUsername,
authlevel: hAuthlevel,
clusters: hClusters,
subClusters: hSubClusters,
roles: hRoles,
},
});
} }
+7 -4
View File
@@ -18,10 +18,13 @@
DropdownItem, DropdownItem,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let clusters; /* Svelte 5 Props */
export let subClusters; let {
export let links; clusters,
export let direction = "down"; subClusters,
links,
direction = "down"
} = $props();
</script> </script>
{#each links as item} {#each links as item}
+9 -6
View File
@@ -25,10 +25,13 @@
Col, Col,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let username; /* Svelte 5 Props */
export let authlevel; let {
export let roles; username,
export let screenSize; authlevel,
roles,
screenSize
} = $props();
</script> </script>
<Nav navbar> <Nav navbar>
@@ -91,7 +94,7 @@
<NavItem> <NavItem>
<Button <Button
outline outline
on:click={() => (window.location.href = "/config")} onclick={() => (window.location.href = "/config")}
style="margin-left: 10px;" style="margin-left: 10px;"
title="Settings" title="Settings"
> >
@@ -136,7 +139,7 @@
<Col xs="4"> <Col xs="4">
<Button <Button
outline outline
on:click={() => (window.location.href = "/config")} onclick={() => (window.location.href = "/config")}
size="sm" size="sm"
class="my-2 w-100" class="my-2 w-100"
> >
+2 -1
View File
@@ -1,7 +1,8 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import Job from './Job.root.svelte' import Job from './Job.root.svelte'
new Job({ mount(Job, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
dbid: jobInfos.id, dbid: jobInfos.id,
+19 -9
View File
@@ -9,7 +9,8 @@
<script> <script>
import { import {
getContext getContext,
onMount
} from "svelte"; } from "svelte";
import { import {
Card, Card,
@@ -21,17 +22,26 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { round } from "mathjs"; import { round } from "mathjs";
export let jobId; /* Svelte 5 Props */
export let jobEnergy = null; let {
export let jobEnergyFootprint = null; jobId,
jobEnergy = null,
jobEnergyFootprint = null
} = $props();
/* Const Init */
const carbonPerkWh = getContext("emission"); const carbonPerkWh = getContext("emission");
let carbonMass;
/* State Init */
let carbonMass = $state(0);
$: if (carbonPerkWh) { /* On Mount */
// ( kWh * g/kWh) / 1000 = kg || Rounded to 2 Digits via [ round(x * 100) / 100 ] onMount(() => {
carbonMass = round( ((jobEnergy ? jobEnergy : 0.0) * carbonPerkWh) / 10 ) / 100; if (carbonPerkWh) {
} // ( kWh * g/kWh) / 1000 = kg || Rounded to 2 Digits via [ round(x * 100) / 100 ]
carbonMass = round( ((jobEnergy ? jobEnergy : 0.0) * carbonPerkWh) / 10 ) / 100;
}
});
</script> </script>
<Card> <Card>
+63 -58
View File
@@ -7,73 +7,78 @@
--> -->
<script> <script>
import { import {
queryStore, queryStore,
gql, gql,
getContextClient getContextClient
} from "@urql/svelte"; } from "@urql/svelte";
import { import {
Card, Card,
Spinner Spinner
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { import {
transformDataForRoofline, transformDataForRoofline,
} from "../generic/utils.js"; } from "../generic/utils.js";
import Roofline from "../generic/plots/Roofline.svelte"; import Roofline from "../generic/plots/Roofline.svelte";
export let job; /* Svelte 5 Props */
export let clusters; let {
job,
let roofWidth; clusters,
} = $props();
const client = getContextClient(); /* Const Init */
const roofQuery = gql` const client = getContextClient();
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) { const roofQuery = gql`
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) { query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
name jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
scope name
metric { scope
series { metric {
data series {
} data
}
}
} }
`; }
}
}`;
// Roofline: Always load roofMetrics with configured timestep (Resolution: 0) /* State Init */
$: roofMetrics = queryStore({ let roofWidth = $state(0);
client: client,
query: roofQuery,
variables: { dbid: job.id, selectedMetrics: ["flops_any", "mem_bw"], selectedScopes: ["node"], selectedResolution: 0 },
});
/* Derived */
// Roofline: Always load roofMetrics with configured timestep (Resolution: 0)
const roofMetrics = $derived(queryStore({
client: client,
query: roofQuery,
variables: { dbid: job.id, selectedMetrics: ["flops_any", "mem_bw"], selectedScopes: ["node"], selectedResolution: 0 },
})
);
</script> </script>
{#if $roofMetrics.error} {#if $roofMetrics.error}
<Card body color="danger">{$roofMetrics.error.message}</Card> <Card body color="danger">{$roofMetrics.error.message}</Card>
{:else if $roofMetrics?.data} {:else if $roofMetrics?.data}
<Card style="height: 400px;"> <Card style="height: 400px;">
<div bind:clientWidth={roofWidth}> <div bind:clientWidth={roofWidth}>
<Roofline <Roofline
width={roofWidth} width={roofWidth}
subCluster={clusters subCluster={clusters
.find((c) => c.name == job.cluster) .find((c) => c.name == job.cluster)
.subClusters.find((sc) => sc.name == job.subCluster)} .subClusters.find((sc) => sc.name == job.subCluster)}
data={transformDataForRoofline( data={transformDataForRoofline(
$roofMetrics.data?.jobMetrics?.find( $roofMetrics.data?.jobMetrics?.find(
(m) => m.name == "flops_any" && m.scope == "node", (m) => m.name == "flops_any" && m.scope == "node",
)?.metric, )?.metric,
$roofMetrics.data?.jobMetrics?.find( $roofMetrics.data?.jobMetrics?.find(
(m) => m.name == "mem_bw" && m.scope == "node", (m) => m.name == "mem_bw" && m.scope == "node",
)?.metric, )?.metric,
)} )}
allowSizeChange allowSizeChange
renderTime renderTime
/> />
</div> </div>
</Card> </Card>
{:else} {:else}
<Spinner secondary /> <Spinner secondary />
{/if} {/if}
+6 -3
View File
@@ -18,9 +18,12 @@
import JobFootprintPolar from "./jobsummary/JobFootprintPolar.svelte"; import JobFootprintPolar from "./jobsummary/JobFootprintPolar.svelte";
export let job; /* Svelte 5 Props */
export let width = "auto"; let {
export let height = "400px"; job,
width = "auto",
height = "400px",
} = $props();
const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`]; const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`];
</script> </script>
+40 -25
View File
@@ -26,16 +26,14 @@
import MetricSelection from "../generic/select/MetricSelection.svelte"; import MetricSelection from "../generic/select/MetricSelection.svelte";
import StatsTable from "./statstab/StatsTable.svelte"; import StatsTable from "./statstab/StatsTable.svelte";
export let job; /* Svelte 5 Props */
export let clusters; let {
export let tabActive; job,
clusters,
let loadScopes = false; tabActive,
let selectedScopes = []; } = $props();
let selectedMetrics = [];
let availableMetrics = new Set(); // For Info Only, filled by MetricSelection Component
let isMetricSelectionOpen = false;
/* Const Init */
const client = getContextClient(); const client = getContextClient();
const query = gql` const query = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) { query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
@@ -55,16 +53,29 @@
} }
`; `;
$: scopedStats = queryStore({ /* State Init */
client: client, let moreScopes = $state(false);
query: query, let selectedScopes = $state([]);
variables: { dbid: job.id, selectedMetrics, selectedScopes }, let selectedMetrics = $state([]);
}); let totalMetrics = $state(0); // For Info Only, filled by MetricSelection Component
let isMetricSelectionOpen = $state(false);
$: if (loadScopes) { /* Derived */
const scopedStats = $derived(queryStore({
client: client,
query: query,
variables: { dbid: job.id, selectedMetrics, selectedScopes },
})
);
/* Functions */
function loadScopes() {
// Archived Jobs Load All Scopes By Default (See Backend)
moreScopes = true;
selectedScopes = ["node", "socket", "core", "hwthread", "accelerator"]; selectedScopes = ["node", "socket", "core", "hwthread", "accelerator"];
} };
/* On Init */
// Handle Job Query on Init -> is not executed anymore // Handle Job Query on Init -> is not executed anymore
getContext("on-init")(() => { getContext("on-init")(() => {
if (!job) return; if (!job) return;
@@ -98,12 +109,12 @@
<TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}> <TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}>
<Row> <Row>
<Col class="m-2"> <Col class="m-2">
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem"> <Button outline onclick={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem">
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available) Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
</Button> </Button>
{#if job.numNodes > 1} {#if job.numNodes > 1 && job.state === "running"}
<Button class="px-2 ml-auto" color="success" outline on:click={() => (loadScopes = !loadScopes)} disabled={loadScopes}> <Button class="px-2 ml-auto" color="success" outline onclick={loadScopes} disabled={moreScopes}>
{#if !loadScopes} {#if !moreScopes}
<Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes <Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes
{:else} {:else}
<Icon name="check-square-fill" style="margin-right:0.25rem"/> OK: Scopes Added <Icon name="check-square-fill" style="margin-right:0.25rem"/> OK: Scopes Added
@@ -129,17 +140,21 @@
{:else} {:else}
<StatsTable <StatsTable
hosts={job.resources.map((r) => r.hostname).sort()} hosts={job.resources.map((r) => r.hostname).sort()}
data={$scopedStats?.data?.scopedJobStats} jobStats={$scopedStats?.data?.scopedJobStats}
{selectedMetrics} {selectedMetrics}
/> />
{/if} {/if}
</TabPane> </TabPane>
<MetricSelection <MetricSelection
bind:isOpen={isMetricSelectionOpen}
bind:totalMetrics
presetMetrics={selectedMetrics}
cluster={job.cluster} cluster={job.cluster}
subCluster={job.subCluster} subCluster={job.subCluster}
configName="job_view_nodestats_selectedMetrics" configName="job_view_nodestats_selectedMetrics"
bind:allMetrics={availableMetrics} preInitialized
bind:metrics={selectedMetrics} applyMetrics={(newMetrics) =>
bind:isOpen={isMetricSelectionOpen} selectedMetrics = [...newMetrics]
}
/> />
@@ -17,79 +17,88 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { findJobFootprintThresholds } from "../../generic/utils.js"; import { findJobFootprintThresholds } from "../../generic/utils.js";
export let job; /* Svelte 5 Props */
let {job} = $props();
/* Derived */
// Prepare Job Footprint Data Based On Values Saved In Database // Prepare Job Footprint Data Based On Values Saved In Database
const jobFootprintData = job?.footprint?.map((jf) => { const jobFootprintData = $derived(buildFootprint(job?.footprint));
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
if (fmc) { /* Functions */
// Unit function buildFootprint(input) {
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "") let result = input?.map((jf) => {
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
// Threshold / -Differences if (fmc) {
const fmt = findJobFootprintThresholds(job, jf.stat, fmc); // Unit
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
// Define basic data -> Value: Use as Provided
const fmBase = { // Threshold / -Differences
name: jf.name, const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
stat: jf.stat,
value: jf.value, // Define basic data -> Value: Use as Provided
unit: unit, const fmBase = {
peak: fmt.peak, name: jf.name,
dir: fmc.lowerIsBetter stat: jf.stat,
}; value: jf.value,
unit: unit,
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) { peak: fmt.peak,
return { dir: fmc.lowerIsBetter
...fmBase,
color: "danger",
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 3
}; };
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
return {
...fmBase,
color: "danger",
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 3
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
return {
...fmBase,
color: "warning",
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Footprint value within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
return {
...fmBase,
color: "info",
message:
"Footprint value above expected normal threshold: Check for artifacts recommended.",
impact: 0,
};
} else {
return {
...fmBase,
color: "secondary",
message:
"Footprint value above expected peak threshold: Check for artifacts!",
impact: -1,
};
}
} else { // No matching metric config: display as single value
return { return {
...fmBase, name: jf.name,
color: "warning", stat: jf.stat,
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`, value: jf.value,
impact: 2,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
return {
...fmBase,
color: "success",
message: "Footprint value within expected thresholds.",
impact: 1,
};
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
return {
...fmBase,
color: "info",
message: message:
"Footprint value above expected normal threshold: Check for artifacts recommended.", `No config for metric ${jf.name} found.`,
impact: 0, impact: 4,
};
} else {
return {
...fmBase,
color: "secondary",
message:
"Footprint value above expected peak threshold: Check for artifacts!",
impact: -1,
}; };
} }
} else { // No matching metric config: display as single value }).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
return { return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
name: jf.name, });;
stat: jf.stat,
value: jf.value, return result
message: };
`No config for metric ${jf.name} found.`,
impact: 4,
};
}
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
});;
function evalFootprint(value, thresholds, lowerIsBetter, level) { function evalFootprint(value, thresholds, lowerIsBetter, level) {
// Handle Metrics in which less value is better // Handle Metrics in which less value is better
@@ -161,7 +170,7 @@
>{fpd.message}</Tooltip >{fpd.message}</Tooltip
> >
</div> </div>
<Row cols={12} class="{(jobFootprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}"> <Row cols={12} class={(jobFootprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}>
{#if fpd.dir} {#if fpd.dir}
<Col xs="1"> <Col xs="1">
<Icon name="caret-left-fill" /> <Icon name="caret-left-fill" />
@@ -20,8 +20,10 @@
import Polar from "../../generic/plots/Polar.svelte"; import Polar from "../../generic/plots/Polar.svelte";
import { findJobFootprintThresholds } from "../../generic/utils.js"; import { findJobFootprintThresholds } from "../../generic/utils.js";
export let job; /* Svelte 5 Props */
let { job } = $props();
/* Const Init */
// Metric Names Configured To Be Footprints For (sub)Cluster // Metric Names Configured To Be Footprints For (sub)Cluster
const clusterFootprintMetrics = getContext("clusters") const clusterFootprintMetrics = getContext("clusters")
.find((c) => c.name == job.cluster)?.subClusters .find((c) => c.name == job.cluster)?.subClusters
@@ -39,23 +41,24 @@
// Pull All Series For Footprint Metrics Statistics Only On Node Scope // Pull All Series For Footprint Metrics Statistics Only On Node Scope
const client = getContextClient(); const client = getContextClient();
const polarQuery = gql` const polarQuery = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!) { query ($dbid: ID!, $selectedMetrics: [String!]!) {
jobStats(id: $dbid, metrics: $selectedMetrics) { jobStats(id: $dbid, metrics: $selectedMetrics) {
name name
data { data {
min min
avg avg
max max
}
} }
} }
}
`; `;
$: polarData = queryStore({ /* Derived */
const polarData = $derived(queryStore({
client: client, client: client,
query: polarQuery, query: polarQuery,
variables:{ dbid: job.id, selectedMetrics: clusterFootprintMetrics }, variables:{ dbid: job.id, selectedMetrics: clusterFootprintMetrics },
}); }));
</script> </script>
<CardBody> <CardBody>
+88 -41
View File
@@ -2,9 +2,9 @@
@component Job-View subcomponent; display table of metric data statistics with selectable scopes @component Job-View subcomponent; display table of metric data statistics with selectable scopes
Properties: Properties:
- `data Object`: The data object
- `selectedMetrics [String]`: The selected metrics
- `hosts [String]`: The list of hostnames of this job - `hosts [String]`: The list of hostnames of this job
- `jobStats Object`: The data object
- `selectedMetrics [String]`: The selected metrics
--> -->
<script> <script>
@@ -17,39 +17,87 @@
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import StatsTableEntry from "./StatsTableEntry.svelte"; import StatsTableEntry from "./StatsTableEntry.svelte";
export let data = []; /* Svelte 5 Props */
export let selectedMetrics = []; let {
export let hosts = []; hosts = [],
jobStats = [],
selectedMetrics = [],
} = $props();
let sorting = {}; /* State Init */
let availableScopes = {}; let sortedHosts = $state(hosts);
let selectedScopes = {}; let sorting = $state(setupSorting(selectedMetrics));
let availableScopes = $state(setupAvailable(jobStats));
let selectedScopes = $state(setupSelected(availableScopes));
const scopesForMetric = (metric) => /* Derived Init */
data?.filter((jm) => jm.name == metric)?.map((jm) => jm.scope) || []; const tableData = $derived(setupData(jobStats, hosts, selectedMetrics, availableScopes))
const setScopeForMetric = (metric, scope) =>
selectedScopes[metric] = scope
$: if (data && selectedMetrics) { /* Functions */
for (let metric of selectedMetrics) { function setupSorting(metrics) {
availableScopes[metric] = scopesForMetric(metric); let pendingSorting = {};
// Set Initial Selection, but do not use selectedScopes: Skips reactivity if (metrics) {
if (availableScopes[metric].includes("accelerator")) { for (let metric of metrics) {
setScopeForMetric(metric, "accelerator"); pendingSorting[metric] = {
} else if (availableScopes[metric].includes("core")) { min: { dir: "up", active: false },
setScopeForMetric(metric, "core"); avg: { dir: "up", active: false },
} else if (availableScopes[metric].includes("socket")) { max: { dir: "up", active: false },
setScopeForMetric(metric, "socket"); };
} else {
setScopeForMetric(metric, "node");
}
sorting[metric] = {
min: { dir: "up", active: false },
avg: { dir: "up", active: false },
max: { dir: "up", active: false },
}; };
} };
return pendingSorting;
};
function setupAvailable(data) {
let pendingAvailable = {};
if (data) {
for (let d of data) {
if (!pendingAvailable[d.name]) {
pendingAvailable[d.name] = [d.scope]
} else {
pendingAvailable[d.name] = [...pendingAvailable[d.name], d.scope]
};
};
};
return pendingAvailable;
};
function setupSelected(available) {
let pendingSelected = {};
for (const [metric, scopes] of Object.entries(available)) {
if (scopes.includes("accelerator")) {
pendingSelected[metric] = "accelerator"
} else if (scopes.includes("core")) {
pendingSelected[metric] = "core"
} else if (scopes.includes("socket")) {
pendingSelected[metric] = "socket"
} else {
pendingSelected[metric] = "node"
};
};
return pendingSelected;
};
function setupData(js, h, sm, as) {
let pendingTableData = {};
if (js) {
for (const host of h) {
if (!pendingTableData[host]) {
pendingTableData[host] = {};
};
for (const metric of sm) {
if (!pendingTableData[host][metric]) {
pendingTableData[host][metric] = {};
};
for (const scope of as[metric]) {
pendingTableData[host][metric][scope] = js.find((d) => d.name == metric && d.scope == scope)
?.stats.filter((st) => st.hostname == host && st.data != null)
?.sort((a, b) => a.id - b.id) || []
};
};
};
};
return pendingTableData;
} }
function sortBy(metric, stat) { function sortBy(metric, stat) {
@@ -62,11 +110,12 @@
s.active = true; s.active = true;
} }
let stats = data.find( let stats = jobStats.find(
(d) => d.name == metric && d.scope == "node", (js) => js.name == metric && js.scope == "node",
)?.stats || []; )?.stats || [];
sorting = { ...sorting }; sorting = { ...sorting };
hosts = hosts.sort((h1, h2) => {
sortedHosts = sortedHosts.sort((h1, h2) => {
let s1 = stats.find((s) => s.hostname == h1)?.data; let s1 = stats.find((s) => s.hostname == h1)?.data;
let s2 = stats.find((s) => s.hostname == h2)?.data; let s2 = stats.find((s) => s.hostname == h2)?.data;
if (s1 == null || s2 == null) return -1; if (s1 == null || s2 == null) return -1;
@@ -81,7 +130,7 @@
<thead> <thead>
<!-- Header Row 1: Selectors --> <!-- Header Row 1: Selectors -->
<tr> <tr>
<th/> <th></th>
{#each selectedMetrics as metric} {#each selectedMetrics as metric}
<!-- To Match Row-2 Header Field Count--> <!-- To Match Row-2 Header Field Count-->
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}> <th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
@@ -89,7 +138,7 @@
<InputGroupText> <InputGroupText>
{metric} {metric}
</InputGroupText> </InputGroupText>
<Input type="select" bind:value={selectedScopes[metric]} disabled={availableScopes[metric].length === 1}> <Input type="select" bind:value={selectedScopes[metric]} disabled={availableScopes[metric]?.length === 1}>
{#each (availableScopes[metric] || []) as scope} {#each (availableScopes[metric] || []) as scope}
<option value={scope}>{scope}</option> <option value={scope}>{scope}</option>
{/each} {/each}
@@ -106,7 +155,7 @@
<th>Id</th> <th>Id</th>
{/if} {/if}
{#each ["min", "avg", "max"] as stat} {#each ["min", "avg", "max"] as stat}
<th on:click={() => sortBy(metric, stat)}> <th onclick={() => sortBy(metric, stat)}>
{stat} {stat}
{#if selectedScopes[metric] == "node"} {#if selectedScopes[metric] == "node"}
<Icon <Icon
@@ -122,14 +171,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each hosts as host (host)} {#each sortedHosts as host (host)}
<tr> <tr>
<th scope="col">{host}</th> <th scope="col">{host}</th>
{#each selectedMetrics as metric (metric)} {#each selectedMetrics as metric (metric)}
<StatsTableEntry <StatsTableEntry
{data} data={tableData[host][metric][selectedScopes[metric]]}
{host}
{metric}
scope={selectedScopes[metric]} scope={selectedScopes[metric]}
/> />
{/each} {/each}
@@ -2,97 +2,83 @@
@component Job-View subcomponent; Single Statistics entry component for statstable @component Job-View subcomponent; Single Statistics entry component for statstable
Properties: Properties:
- `host String`: The hostname (== node) - `data [Object]`: The jobs statsdata for host-metric-scope
- `metric String`: The metric name
- `scope String`: The selected scope - `scope String`: The selected scope
- `data [Object]`: The jobs statsdata
--> -->
<script> <script>
import { Icon } from "@sveltestrap/sveltestrap"; import { Icon } from "@sveltestrap/sveltestrap";
export let host; /* Svelte 5 Props */
export let metric; let {
export let scope; data,
export let data; scope,
} = $props();
let entrySorting = { /* State Init */
id: { dir: "down", active: true }, let sortBy = $state("id");
min: { dir: "up", active: false }, let sortDir = $state("down");
avg: { dir: "up", active: false },
max: { dir: "up", active: false },
};
function compareNumbers(a, b) { /* Derived */
return a.id - b.id; const sortedData = $derived(updateData(data, sortBy, sortDir));
}
function sortByField(field) { /* Functions */
let s = entrySorting[field]; function updateData(data, sortBy, sortDir) {
if (s.active) { data.sort((a, b) => {
s.dir = s.dir == "up" ? "down" : "up"; if (a == null || b == null) {
} else { return -1;
for (let field in entrySorting) entrySorting[field].active = false; } else if (sortBy === "id") {
s.active = true; return sortDir != "up"
} ? a[sortBy].localeCompare(b[sortBy], undefined, {numeric: true, sensitivity: 'base'})
: b[sortBy].localeCompare(a[sortBy], undefined, {numeric: true, sensitivity: 'base'});
entrySorting = { ...entrySorting };
stats = stats.sort((a, b) => {
if (a == null || b == null) return -1;
if (field === "id") {
return s.dir != "up" ?
a[field].localeCompare(b[field], undefined, {numeric: true, sensitivity: 'base'}) :
b[field].localeCompare(a[field], undefined, {numeric: true, sensitivity: 'base'})
} else { } else {
return s.dir != "up" return sortDir != "up"
? a.data[field] - b.data[field] ? a.data[sortBy] - b.data[sortBy]
: b.data[field] - a.data[field]; : b.data[sortBy] - a.data[sortBy];
} };
}); });
} return [...data];
};
$: stats = data
?.find((d) => d.name == metric && d.scope == scope)
?.stats.filter((s) => s.hostname == host && s.data != null)
?.sort(compareNumbers) || [];
</script> </script>
{#if stats == null || stats.length == 0} {#if data == null || data.length == 0}
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td> <td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
{:else if stats.length == 1 && scope == "node"} {:else if data.length == 1 && scope == "node"}
<td> <td>
{stats[0].data.min} {data[0].data.min}
</td> </td>
<td> <td>
{stats[0].data.avg} {data[0].data.avg}
</td> </td>
<td> <td>
{stats[0].data.max} {data[0].data.max}
</td> </td>
{:else} {:else}
<td colspan="4"> <td colspan="4">
<table style="width: 100%;"> <table style="width: 100%;">
<tr> <tbody>
{#each ["id", "min", "avg", "max"] as field}
<th on:click={() => sortByField(field)}>
Sort
<Icon
name="caret-{entrySorting[field].dir}{entrySorting[field].active
? '-fill'
: ''}"
/>
</th>
{/each}
</tr>
{#each stats as s, i}
<tr> <tr>
<th>{s.id ?? i}</th> {#each ["id", "min", "avg", "max"] as field}
<td>{s.data.min}</td> <th onclick={() => {
<td>{s.data.avg}</td> sortBy = field;
<td>{s.data.max}</td> sortDir = (sortDir == "up" ? "down" : "up");
}}>
Sort
<Icon
name="caret-{sortBy == field? sortDir: 'down'}{sortBy == field? '-fill': ''}"
/>
</th>
{/each}
</tr> </tr>
{/each} {#each sortedData as s, i}
<tr>
<th>{s.id ?? i}</th>
<td>{s.data.min}</td>
<td>{s.data.avg}</td>
<td>{s.data.max}</td>
</tr>
{/each}
</tbody>
</table> </table>
</td> </td>
{/if} {/if}
+2 -1
View File
@@ -1,7 +1,8 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import Jobs from './Jobs.root.svelte' import Jobs from './Jobs.root.svelte'
new Jobs({ mount(Jobs, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
filterPresets: filterPresets, filterPresets: filterPresets,
+2 -1
View File
@@ -1,7 +1,8 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import List from './List.root.svelte' import List from './List.root.svelte'
new List({ mount(List, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
filterPresets: filterPresets, filterPresets: filterPresets,
+4 -3
View File
@@ -1,13 +1,14 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import Node from './Node.root.svelte' import Node from './Node.root.svelte'
new Node({ mount(Node, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
cluster: infos.cluster, cluster: infos.cluster,
hostname: infos.hostname, hostname: infos.hostname,
from: infos.from, presetFrom: infos.from,
to: infos.to presetTo: infos.to
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig] ['cc-config', clusterCockpitConfig]
+2 -1
View File
@@ -1,7 +1,8 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import Status from './Status.root.svelte' import Status from './Status.root.svelte'
new Status({ mount(Status, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
cluster: infos.cluster, cluster: infos.cluster,
+4 -3
View File
@@ -1,14 +1,15 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import Systems from './Systems.root.svelte' import Systems from './Systems.root.svelte'
new Systems({ mount(Systems, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
displayType: displayType, displayType: displayType,
cluster: infos.cluster, cluster: infos.cluster,
subCluster: infos.subCluster, subCluster: infos.subCluster,
from: infos.from, fromPreset: infos.from,
to: infos.to toPreset: infos.to
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig], ['cc-config', clusterCockpitConfig],
+112 -87
View File
@@ -10,38 +10,28 @@
--> -->
<script> <script>
import { getContext } from "svelte";
import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte"; import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte";
import { Row, Col, Card, Table, Spinner } from "@sveltestrap/sveltestrap"; import { Row, Col, Card, Table, Spinner } from "@sveltestrap/sveltestrap";
import { stickyHeader } from "../generic/utils.js"; import { stickyHeader } from "../generic/utils.js";
import NodeListRow from "./nodelist/NodeListRow.svelte"; import NodeListRow from "./nodelist/NodeListRow.svelte";
import Pagination from "../generic/joblist/Pagination.svelte"; import Pagination from "../generic/joblist/Pagination.svelte";
export let cluster; /* Svelte 5 Props */
export let subCluster = ""; let {
export let ccconfig = null; cluster,
export let selectedMetrics = []; subCluster = "",
export let selectedResolution = 0; ccconfig = null,
export let hostnameFilter = ""; selectedMetrics = [],
export let systemUnits = null; selectedResolution = 0,
export let from = null; hostnameFilter = "",
export let to = null; presetSystemUnits = null,
from = null,
to = null
} = $props();
// Decouple from Job List Paging Params? /* Const Init */
let usePaging = ccconfig?.node_list_usePaging || false
let itemsPerPage = usePaging ? (ccconfig?.plot_list_nodesPerPage || 10) : 10;
let page = 1;
let paging = { itemsPerPage, page };
let headerPaddingTop = 0;
stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x),
);
// const { query: initq } = init();
const initialized = getContext("initialized");
const client = getContextClient(); const client = getContextClient();
const usePaging = ccconfig?.node_list_usePaging || false;
const nodeListQuery = gql` const nodeListQuery = gql`
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) { query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
nodeMetricsList( nodeMetricsList(
@@ -91,51 +81,16 @@
} }
` `
const updateConfigurationMutation = ({ name, value }) => { /* State Init */
return mutationStore({ let nodes = $state([]);
client: client, let page = $state(1);
query: gql` let itemsPerPage = $state(usePaging ? (ccconfig?.plot_list_nodesPerPage || 10) : 10);
mutation ($name: String!, $value: String!) { let headerPaddingTop = $state(0);
updateConfiguration(name: $name, value: $value) let matchedNodes = $state(0);
}
`,
variables: { name, value },
});
};
// Decouple from Job List Paging Params? /* Derived */
function updateConfiguration(value, page) { const paging = $derived({ itemsPerPage, page });
updateConfigurationMutation({ const nodesQuery = $derived(queryStore({
name: "plot_list_nodesPerPage",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
nodes = [] // Empty List
paging = { itemsPerPage: value, page: page }; // Trigger reload of nodeList
} else if (res.fetching === false && res.error) {
throw res.error;
}
});
}
if (!usePaging) {
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $nodesQuery?.data != null && $nodesQuery.data?.nodeMetricsList.hasNextPage) {
let pendingPaging = { ...paging }
pendingPaging.page += 1
paging = pendingPaging
};
});
};
$: nodesQuery = queryStore({
client: client, client: client,
query: nodeListQuery, query: nodeListQuery,
variables: { variables: {
@@ -150,24 +105,93 @@
selectedResolution: selectedResolution, selectedResolution: selectedResolution,
}, },
requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change
}));
/* Effects */
$effect(() => {
if (!usePaging) {
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $nodesQuery?.data?.nodeMetricsList?.hasNextPage) {
page += 1
};
});
};
}); });
let nodes = []; $effect(() => {
$: if ($initialized && $nodesQuery.data) { handleNodes($nodesQuery?.data?.nodeMetricsList);
if (usePaging) { });
nodes = [...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host));
} else { $effect(() => {
nodes = nodes.concat([...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host))) // Triggers (Except Paging)
from, to
selectedMetrics, selectedResolution
hostnameFilter
// Continous Scroll: Reset nodes and paging if parameters change: Existing entries will not match new selections
if (!usePaging) {
nodes = [];
page = 1;
} }
});
/* Functions */
function handleNodes(data) {
if (data) {
matchedNodes = data.totalNodes;
if (usePaging || nodes.length == 0) {
nodes = [...data.items].sort((a, b) => a.host.localeCompare(b.host));
} else {
// Workaround to ignore secondary store triggers (reason tbd)
const oldNodes = $state.snapshot(nodes)
const newNodes = [...data.items].map((d) => d.host)
if (!oldNodes.some((n) => newNodes.includes(n.host))) {
nodes = nodes.concat([...data.items].sort((a, b) => a.host.localeCompare(b.host)))
};
};
};
};
// Decouple from Job List Paging Params?
function updateConfiguration(newItems, newPage) {
updateConfigurationMutation({
name: "plot_list_nodesPerPage",
value: newItems.toString(),
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
nodes = []; // Empty List
itemsPerPage = newItems;
page = newPage; // Trigger reload of nodeList
} else if (res.fetching === false && res.error) {
throw res.error;
}
});
} }
$: if (!usePaging && (selectedMetrics || selectedResolution || hostnameFilter || from || to)) { /* Const Functions */
// Continous Scroll: Reset list and paging if parameters change: Existing entries will not match new selections const updateConfigurationMutation = ({ name, value }) => {
nodes = []; return mutationStore({
paging = { itemsPerPage, page: 1 }; client: client,
} query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
$: matchedNodes = $nodesQuery.data?.nodeMetricsList.totalNodes || matchedNodes; /* Init Header */
stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x),
);
</script> </script>
<Row> <Row>
@@ -192,7 +216,7 @@
scope="col" scope="col"
style="padding-top: {headerPaddingTop}px" style="padding-top: {headerPaddingTop}px"
> >
{metric} ({systemUnits[metric]}) {metric} ({presetSystemUnits[metric]})
</th> </th>
{/each} {/each}
</tr> </tr>
@@ -212,7 +236,7 @@
<td colspan={selectedMetrics.length + 1}> No nodes found </td> <td colspan={selectedMetrics.length + 1}> No nodes found </td>
</tr> </tr>
{/each} {/each}
{/if} {/if}
{#if $nodesQuery.fetching || !$nodesQuery.data} {#if $nodesQuery.fetching || !$nodesQuery.data}
<tr> <tr>
<td colspan={selectedMetrics.length + 1}> <td colspan={selectedMetrics.length + 1}>
@@ -238,16 +262,17 @@
{#if usePaging} {#if usePaging}
<Pagination <Pagination
bind:page {page}
{itemsPerPage} {itemsPerPage}
itemText="Nodes" itemText="Nodes"
totalItems={matchedNodes} totalItems={matchedNodes}
on:update-paging={({ detail }) => { updatePaging={(detail) => {
if (detail.itemsPerPage != itemsPerPage) { if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page); updateConfiguration(detail.itemsPerPage, detail.page);
} else { } else {
nodes = [] nodes = [];
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page }; itemsPerPage = detail.itemsPerPage;
page = detail.page;
} }
}} }}
/> />
+92 -91
View File
@@ -10,101 +10,99 @@
<script> <script>
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { queryStore, gql, getContextClient } from "@urql/svelte";
import { Row, Col, Card, Spinner } from "@sveltestrap/sveltestrap"; import { Row, Col, Card, Spinner } from "@sveltestrap/sveltestrap";
import { init, checkMetricsDisabled } from "../generic/utils.js"; import { checkMetricDisabled } from "../generic/utils.js";
import MetricPlot from "../generic/plots/MetricPlot.svelte"; import MetricPlot from "../generic/plots/MetricPlot.svelte";
export let ccconfig = null; /* Svelte 5 Props */
export let cluster = ""; let {
export const subCluster = ""; ccconfig = null,
export let selectedMetrics = null; cluster = "",
export let hostnameFilter = ""; selectedMetric = "",
export let from = null; hostnameFilter = "",
export let to = null; from = null,
to = null
} = $props();
const { query: initq } = init(); /* Const Init */
const client = getContextClient(); const client = getContextClient();
const nodeQuery = gql`
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) { /* Derived */
nodeMetrics( const nodesQuery = $derived(queryStore({
cluster: $cluster client: client,
metrics: $metrics query: gql`
from: $from query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
to: $to nodeMetrics(
) { cluster: $cluster
host metrics: $metrics
subCluster from: $from
metrics { to: $to
name ) {
scope host
metric { subCluster
timestep metrics {
unit { name
base scope
prefix metric {
} timestep
series { unit {
statistics { base
min prefix
avg }
max series {
statistics {
min
avg
max
}
data
} }
data
} }
} }
} }
} }
} `,
`
$: selectedMetric = selectedMetrics[0] ? selectedMetrics[0] : "";
$: nodesQuery = queryStore({
client: client,
query: nodeQuery,
variables: { variables: {
cluster: cluster, cluster: cluster,
metrics: selectedMetrics, metrics: [selectedMetric],
from: from.toISOString(), from: from,
to: to.toISOString(), to: to,
}, },
}); }));
let rawData = [] const mappedData = $derived(handleQueryData($nodesQuery?.data));
$: if ($initq.data && $nodesQuery?.data) { const filteredData = $derived(mappedData.filter((h) => h.host.includes(hostnameFilter)));
rawData = $nodesQuery?.data?.nodeMetrics.filter((h) => {
if (h.subCluster === '') { // Exclude nodes with empty subCluster field
console.warn('subCluster not configured for node', h.host)
return false
} else {
return h.metrics.some(
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
)
}
})
}
let mappedData = [] /* Functions */
$: if (rawData?.length > 0) { function handleQueryData(queryData) {
mappedData = rawData.map((h) => ({ let rawData = []
host: h.host, if (queryData) {
subCluster: h.subCluster, rawData = queryData.nodeMetrics.filter((h) => {
data: h.metrics.filter( if (h.subCluster !== '') { // Exclude nodes with empty subCluster field
(m) => selectedMetrics.includes(m.name) && m.scope == "node", return h.metrics.some(
), (m) => m?.name == selectedMetric && m.scope == "node",
disabled: checkMetricsDisabled( )
selectedMetrics, };
cluster, });
h.subCluster, };
),
})) let pendingMapped = [];
.sort((a, b) => a.host.localeCompare(b.host)) if (rawData.length > 0) {
} pendingMapped = rawData.map((h) => ({
host: h.host,
let filteredData = [] subCluster: h.subCluster,
$: if (mappedData?.length > 0) { data: h.metrics.filter(
filteredData = mappedData.filter((h) => (m) => m?.name == selectedMetric && m.scope == "node",
h.host.includes(hostnameFilter) ),
) disabled: checkMetricDisabled(
selectedMetric,
cluster,
h.subCluster,
),
}))
.sort((a, b) => a.host.localeCompare(b.host))
}
return pendingMapped;
} }
</script> </script>
@@ -132,22 +130,25 @@
>{item.host} ({item.subCluster})</a >{item.host} ({item.subCluster})</a
> >
</h4> </h4>
{#if item?.disabled[selectedMetric]} {#if item?.disabled}
<Card body class="mx-3" color="info" <Card body class="mx-3" color="info"
>Metric disabled for subcluster <code >Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code >{selectedMetric}:{item.subCluster}</code
></Card ></Card
> >
{:else} {:else}
<!-- "No Data"-Warning included in MetricPlot-Component --> <!-- "No Data"-Warning included in MetricPlot-Component -->
<MetricPlot <!-- #key: X-axis keeps last selected timerange otherwise -->
timestep={item.data[0].metric.timestep} {#key item.data[0].metric.series[0].data.length}
series={item.data[0].metric.series} <MetricPlot
metric={item.data[0].name} timestep={item.data[0].metric.timestep}
{cluster} series={item.data[0].metric.series}
subCluster={item.subCluster} metric={item.data[0].name}
forNode {cluster}
/> subCluster={item.subCluster}
forNode
/>
{/key}
{/if} {/if}
</Col> </Col>
{/each} {/each}
@@ -158,7 +158,7 @@
height={175} height={175}
forNode forNode
/> />
<div class="my-2"/> <div class="my-2"></div>
{#key extendedLegendData} {#key extendedLegendData}
<MetricPlot <MetricPlot
{cluster} {cluster}
+3 -4
View File
@@ -1,16 +1,15 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import Tags from './Tags.root.svelte' import Tags from './Tags.root.svelte'
new Tags({ mount(Tags, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
username: username, username: username,
isAdmin: isAdmin, isAdmin: isAdmin,
tagmap: tagmap, presetTagmap: tagmap,
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig] ['cc-config', clusterCockpitConfig]
]) ])
}) })
+2 -1
View File
@@ -1,7 +1,8 @@
import { mount } from 'svelte';
import {} from './header.entrypoint.js' import {} from './header.entrypoint.js'
import User from './User.root.svelte' import User from './User.root.svelte'
new User({ mount(User, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
filterPresets: filterPresets, filterPresets: filterPresets,
+6 -7
View File
@@ -14,13 +14,12 @@
{{block "stylesheets" .}} {{block "stylesheets" .}}
{{end}} {{end}}
<script> <script>
const header = { // Used for header.entrypoint.js mount and filters/Resources.svelte
"username": "{{ .User.Username }}", const hUsername = {{ .User.Username }};
"authlevel": {{ .User.GetAuthLevel }}, const hAuthlevel = {{ .User.GetAuthLevel }};
"clusters": {{ .Clusters }}, const hClusters = {{ .Clusters }};
"subClusters": {{ .SubClusters }}, const hSubClusters = {{ .SubClusters }};
"roles": {{ .Roles }} const hRoles = {{ .Roles }};
};
</script> </script>
</head> </head>
<body class="site"> <body class="site">