Migrate job view stats table

This commit is contained in:
Christoph Kluge 2025-06-04 11:28:45 +02:00
parent 927e25c72c
commit 5e696c10d5
3 changed files with 126 additions and 94 deletions

View File

@ -129,7 +129,7 @@
{: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}
@ -142,6 +142,7 @@
cluster={job.cluster} cluster={job.cluster}
subCluster={job.subCluster} subCluster={job.subCluster}
configName="job_view_nodestats_selectedMetrics" configName="job_view_nodestats_selectedMetrics"
preInitialized
applyMetrics={(newMetrics) => applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics] selectedMetrics = [...newMetrics]
} }

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")) {
setScopeForMetric(metric, "core");
} else if (availableScopes[metric].includes("socket")) {
setScopeForMetric(metric, "socket");
} else {
setScopeForMetric(metric, "node");
}
sorting[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 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;
@ -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}

View File

@ -2,73 +2,56 @@
@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 }, /* Derived */
const sortedData = $derived(updateData(data, sortBy, sortDir));
/* Functions */
function updateData(data, sortBy, sortDir) {
data.sort((a, b) => {
if (a == null || b == null) {
return -1;
} else if (sortBy === "id") {
return sortDir != "up"
? a[sortBy].localeCompare(b[sortBy], undefined, {numeric: true, sensitivity: 'base'})
: b[sortBy].localeCompare(a[sortBy], undefined, {numeric: true, sensitivity: 'base'});
} else {
return sortDir != "up"
? a.data[sortBy] - b.data[sortBy]
: b.data[sortBy] - a.data[sortBy];
}; };
function compareNumbers(a, b) {
return a.id - b.id;
}
function sortByField(field) {
let s = entrySorting[field];
if (s.active) {
s.dir = s.dir == "up" ? "down" : "up";
} else {
for (let field in entrySorting) entrySorting[field].active = false;
s.active = true;
}
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 {
return s.dir != "up"
? a.data[field] - b.data[field]
: b.data[field] - a.data[field];
}
}); });
} 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">
@ -76,17 +59,18 @@
<tbody> <tbody>
<tr> <tr>
{#each ["id", "min", "avg", "max"] as field} {#each ["id", "min", "avg", "max"] as field}
<th on:click={() => sortByField(field)}> <th onclick={() => {
sortBy = field;
sortDir = (sortDir == "up" ? "down" : "up");
}}>
Sort Sort
<Icon <Icon
name="caret-{entrySorting[field].dir}{entrySorting[field].active name="caret-{sortBy == field? sortDir: 'down'}{sortBy == field? '-fill': ''}"
? '-fill'
: ''}"
/> />
</th> </th>
{/each} {/each}
</tr> </tr>
{#each stats as s, i} {#each sortedData as s, i}
<tr> <tr>
<th>{s.id ?? i}</th> <th>{s.id ?? i}</th>
<td>{s.data.min}</td> <td>{s.data.min}</td>