mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-06-08 08:33:49 +02:00
Migrate job view stats table
This commit is contained in:
parent
927e25c72c
commit
5e696c10d5
@ -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]
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
@ -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,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 },
|
|
||||||
};
|
|
||||||
|
|
||||||
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">
|
||||||
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user