mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-06-08 00:23:48 +02:00
Migrate job view stats table
This commit is contained in:
parent
927e25c72c
commit
5e696c10d5
@ -129,7 +129,7 @@
|
||||
{:else}
|
||||
<StatsTable
|
||||
hosts={job.resources.map((r) => r.hostname).sort()}
|
||||
data={$scopedStats?.data?.scopedJobStats}
|
||||
jobStats={$scopedStats?.data?.scopedJobStats}
|
||||
{selectedMetrics}
|
||||
/>
|
||||
{/if}
|
||||
@ -142,6 +142,7 @@
|
||||
cluster={job.cluster}
|
||||
subCluster={job.subCluster}
|
||||
configName="job_view_nodestats_selectedMetrics"
|
||||
preInitialized
|
||||
applyMetrics={(newMetrics) =>
|
||||
selectedMetrics = [...newMetrics]
|
||||
}
|
||||
|
@ -2,9 +2,9 @@
|
||||
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
|
||||
|
||||
Properties:
|
||||
- `data Object`: The data object
|
||||
- `selectedMetrics [String]`: The selected metrics
|
||||
- `hosts [String]`: The list of hostnames of this job
|
||||
- `jobStats Object`: The data object
|
||||
- `selectedMetrics [String]`: The selected metrics
|
||||
-->
|
||||
|
||||
<script>
|
||||
@ -17,39 +17,87 @@
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import StatsTableEntry from "./StatsTableEntry.svelte";
|
||||
|
||||
export let data = [];
|
||||
export let selectedMetrics = [];
|
||||
export let hosts = [];
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
hosts = [],
|
||||
jobStats = [],
|
||||
selectedMetrics = [],
|
||||
} = $props();
|
||||
|
||||
let sorting = {};
|
||||
let availableScopes = {};
|
||||
let selectedScopes = {};
|
||||
/* State Init */
|
||||
let sortedHosts = $state(hosts);
|
||||
let sorting = $state(setupSorting(selectedMetrics));
|
||||
let availableScopes = $state(setupAvailable(jobStats));
|
||||
let selectedScopes = $state(setupSelected(availableScopes));
|
||||
|
||||
const scopesForMetric = (metric) =>
|
||||
data?.filter((jm) => jm.name == metric)?.map((jm) => jm.scope) || [];
|
||||
const setScopeForMetric = (metric, scope) =>
|
||||
selectedScopes[metric] = scope
|
||||
/* Derived Init */
|
||||
const tableData = $derived(setupData(jobStats, hosts, selectedMetrics, availableScopes))
|
||||
|
||||
$: if (data && selectedMetrics) {
|
||||
for (let metric of selectedMetrics) {
|
||||
availableScopes[metric] = scopesForMetric(metric);
|
||||
// Set Initial Selection, but do not use selectedScopes: Skips reactivity
|
||||
if (availableScopes[metric].includes("accelerator")) {
|
||||
setScopeForMetric(metric, "accelerator");
|
||||
} 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 },
|
||||
avg: { dir: "up", active: false },
|
||||
max: { dir: "up", active: false },
|
||||
/* Functions */
|
||||
function setupSorting(metrics) {
|
||||
let pendingSorting = {};
|
||||
if (metrics) {
|
||||
for (let metric of metrics) {
|
||||
pendingSorting[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) {
|
||||
@ -62,11 +110,12 @@
|
||||
s.active = true;
|
||||
}
|
||||
|
||||
let stats = data.find(
|
||||
(d) => d.name == metric && d.scope == "node",
|
||||
let stats = jobStats.find(
|
||||
(js) => js.name == metric && js.scope == "node",
|
||||
)?.stats || [];
|
||||
sorting = { ...sorting };
|
||||
hosts = hosts.sort((h1, h2) => {
|
||||
|
||||
sortedHosts = sortedHosts.sort((h1, h2) => {
|
||||
let s1 = stats.find((s) => s.hostname == h1)?.data;
|
||||
let s2 = stats.find((s) => s.hostname == h2)?.data;
|
||||
if (s1 == null || s2 == null) return -1;
|
||||
@ -89,7 +138,7 @@
|
||||
<InputGroupText>
|
||||
{metric}
|
||||
</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}
|
||||
<option value={scope}>{scope}</option>
|
||||
{/each}
|
||||
@ -106,7 +155,7 @@
|
||||
<th>Id</th>
|
||||
{/if}
|
||||
{#each ["min", "avg", "max"] as stat}
|
||||
<th on:click={() => sortBy(metric, stat)}>
|
||||
<th onclick={() => sortBy(metric, stat)}>
|
||||
{stat}
|
||||
{#if selectedScopes[metric] == "node"}
|
||||
<Icon
|
||||
@ -122,14 +171,12 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each hosts as host (host)}
|
||||
{#each sortedHosts as host (host)}
|
||||
<tr>
|
||||
<th scope="col">{host}</th>
|
||||
{#each selectedMetrics as metric (metric)}
|
||||
<StatsTableEntry
|
||||
{data}
|
||||
{host}
|
||||
{metric}
|
||||
data={tableData[host][metric][selectedScopes[metric]]}
|
||||
scope={selectedScopes[metric]}
|
||||
/>
|
||||
{/each}
|
||||
|
@ -2,73 +2,56 @@
|
||||
@component Job-View subcomponent; Single Statistics entry component for statstable
|
||||
|
||||
Properties:
|
||||
- `host String`: The hostname (== node)
|
||||
- `metric String`: The metric name
|
||||
- `data [Object]`: The jobs statsdata for host-metric-scope
|
||||
- `scope String`: The selected scope
|
||||
- `data [Object]`: The jobs statsdata
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let host;
|
||||
export let metric;
|
||||
export let scope;
|
||||
export let data;
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
data,
|
||||
scope,
|
||||
} = $props();
|
||||
|
||||
let entrySorting = {
|
||||
id: { dir: "down", active: true },
|
||||
min: { dir: "up", active: false },
|
||||
avg: { dir: "up", active: false },
|
||||
max: { dir: "up", active: false },
|
||||
};
|
||||
/* State Init */
|
||||
let sortBy = $state("id");
|
||||
let sortDir = $state("down");
|
||||
|
||||
function compareNumbers(a, b) {
|
||||
return a.id - b.id;
|
||||
}
|
||||
/* Derived */
|
||||
const sortedData = $derived(updateData(data, sortBy, sortDir));
|
||||
|
||||
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'})
|
||||
/* 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 s.dir != "up"
|
||||
? a.data[field] - b.data[field]
|
||||
: b.data[field] - a.data[field];
|
||||
}
|
||||
return sortDir != "up"
|
||||
? a.data[sortBy] - b.data[sortBy]
|
||||
: b.data[sortBy] - a.data[sortBy];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
$: stats = data
|
||||
?.find((d) => d.name == metric && d.scope == scope)
|
||||
?.stats.filter((s) => s.hostname == host && s.data != null)
|
||||
?.sort(compareNumbers) || [];
|
||||
return [...data];
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if stats == null || stats.length == 0}
|
||||
{#if data == null || data.length == 0}
|
||||
<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>
|
||||
{stats[0].data.min}
|
||||
{data[0].data.min}
|
||||
</td>
|
||||
<td>
|
||||
{stats[0].data.avg}
|
||||
{data[0].data.avg}
|
||||
</td>
|
||||
<td>
|
||||
{stats[0].data.max}
|
||||
{data[0].data.max}
|
||||
</td>
|
||||
{:else}
|
||||
<td colspan="4">
|
||||
@ -76,17 +59,18 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
{#each ["id", "min", "avg", "max"] as field}
|
||||
<th on:click={() => sortByField(field)}>
|
||||
<th onclick={() => {
|
||||
sortBy = field;
|
||||
sortDir = (sortDir == "up" ? "down" : "up");
|
||||
}}>
|
||||
Sort
|
||||
<Icon
|
||||
name="caret-{entrySorting[field].dir}{entrySorting[field].active
|
||||
? '-fill'
|
||||
: ''}"
|
||||
name="caret-{sortBy == field? sortDir: 'down'}{sortBy == field? '-fill': ''}"
|
||||
/>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
{#each stats as s, i}
|
||||
{#each sortedData as s, i}
|
||||
<tr>
|
||||
<th>{s.id ?? i}</th>
|
||||
<td>{s.data.min}</td>
|
||||
|
Loading…
x
Reference in New Issue
Block a user