add extended legend for nodelist acc metrics, move nodelist paging select

This commit is contained in:
Christoph Kluge 2025-01-21 18:35:03 +01:00
parent d0580592be
commit 735988decb
10 changed files with 386 additions and 269 deletions

View File

@ -3,6 +3,7 @@
Properties: Properties:
- `ìsAdmin Bool!`: Is currently logged in user admin authority - `ìsAdmin Bool!`: Is currently logged in user admin authority
- `isSupport Bool!`: Is currently logged in user support authority
- `isApi Bool!`: Is currently logged in user api authority - `isApi Bool!`: Is currently logged in user api authority
- `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
--> -->
@ -10,15 +11,17 @@
<script> <script>
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap"; import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
import UserSettings from "./config/UserSettings.svelte"; import UserSettings from "./config/UserSettings.svelte";
import SupportSettings from "./config/SupportSettings.svelte";
import AdminSettings from "./config/AdminSettings.svelte"; import AdminSettings from "./config/AdminSettings.svelte";
export let isAdmin; export let isAdmin;
export let isSupport;
export let isApi; export let isApi;
export let username; export let username;
export let ncontent; export let ncontent;
</script> </script>
{#if isAdmin == true} {#if isAdmin}
<Card style="margin-bottom: 1.5em;"> <Card style="margin-bottom: 1.5em;">
<CardHeader> <CardHeader>
<CardTitle class="mb-1">Admin Options</CardTitle> <CardTitle class="mb-1">Admin Options</CardTitle>
@ -27,6 +30,15 @@
</Card> </Card>
{/if} {/if}
{#if isSupport || isAdmin}
<Card style="margin-bottom: 1.5em;">
<CardHeader>
<CardTitle class="mb-1">Support Options</CardTitle>
</CardHeader>
<SupportSettings/>
</Card>
{/if}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle class="mb-1">User Options</CardTitle> <CardTitle class="mb-1">User Options</CardTitle>

View File

@ -9,7 +9,7 @@
--> -->
<script> <script>
import { getContext, createEventDispatcher } from "svelte"; import { getContext } from "svelte";
import { import {
Row, Row,
Col, Col,
@ -64,10 +64,8 @@
let isMetricsSelectionOpen = false; let isMetricsSelectionOpen = false;
/* /*
Note 1: Scope Selector or Auto-Scoped? -> USeful auto scoping with stats view where applicable -> CHeck with JVe 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: "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 3: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
Note 4: Resolution changes as implemented only possible for all plots generally, not for individual metrics: Result list if build from GQL result *including* metric series
*/ */
let systemMetrics = []; let systemMetrics = [];

View File

@ -5,6 +5,7 @@ new Config({
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
isAdmin: isAdmin, isAdmin: isAdmin,
isSupport: isSupport,
isApi: isApi, isApi: isApi,
username: username, username: username,
ncontent: ncontent, ncontent: ncontent,

View File

@ -0,0 +1,13 @@
<!--
@component Support settings wrapper
Properties: None
-->
<script>
import { getContext } from "svelte";
import SupportOptions from "./support/SupportOptions.svelte";
const ccconfig = getContext("cc-config");
</script>
<SupportOptions config={ccconfig}/>

View File

@ -4,13 +4,8 @@
<script> <script>
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
import { Col, Card, CardBody, CardTitle, Button} from "@sveltestrap/sveltestrap"; import { Col, Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition";
export let config;
let message;
let displayMessage;
let scrambled; let scrambled;
const resampleConfig = getContext("resampling"); const resampleConfig = getContext("resampling");
@ -28,33 +23,6 @@
window.localStorage.removeItem("cc-scramble-names"); window.localStorage.removeItem("cc-scramble-names");
} }
} }
async function handleSettingSubmit(selector, target) {
let form = document.querySelector(selector);
let formData = new FormData(form);
try {
const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) {
let text = await res.text();
popMessage(text, target, "#048109");
} else {
let text = await res.text();
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, target, "#d63384");
}
return false;
}
function popMessage(response, restarget, rescolor) {
message = { msg: response, target: restarget, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
</script> </script>
<Col> <Col>
@ -73,53 +41,6 @@
</Card> </Card>
</Col> </Col>
<Col>
<Card class="h-100">
<form
id="node-paging-form"
method="post"
action="/frontend/configuration/"
class="card-body"
on:submit|preventDefault={() =>
handleSettingSubmit("#node-paging-form", "npag")}
>
<!-- 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
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="node_list_usePaging" />
<div class="mb-3">
<div>
{#if config?.node_list_usePaging}
<input type="radio" id="nodes-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="nodes-true" name="value" value="true" />
{/if}
<label for="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="false">Continuous scroll iteratively adding 10 nodes.</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card>
</Col>
{#if resampleConfig} {#if resampleConfig}
<Col> <Col>
<Card class="h-100"> <Card class="h-100">

View File

@ -0,0 +1,89 @@
<!--
@component Support option select card
-->
<script>
import { Row, Col, Card, CardTitle, Button} from "@sveltestrap/sveltestrap";
import { fade } from "svelte/transition";
export let config;
let message;
let displayMessage;
async function handleSettingSubmit(selector, target) {
let form = document.querySelector(selector);
let formData = new FormData(form);
try {
const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) {
let text = await res.text();
popMessage(text, target, "#048109");
} else {
let text = await res.text();
throw new Error("Response Code " + res.status + "-> " + text);
}
} catch (err) {
popMessage(err, target, "#d63384");
}
return false;
}
function popMessage(response, restarget, rescolor) {
message = { msg: response, target: restarget, color: rescolor };
displayMessage = true;
setTimeout(function () {
displayMessage = false;
}, 3500);
}
</script>
<Row cols={1} class="p-2 g-2">
<Col>
<Card class="h-100">
<form
id="node-paging-form"
method="post"
action="/frontend/configuration/"
class="card-body"
on:submit|preventDefault={() =>
handleSettingSubmit("#node-paging-form", "npag")}
>
<!-- 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
>Update: {message.msg}</code
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="node_list_usePaging" />
<div class="mb-3">
<div>
{#if config?.node_list_usePaging}
<input type="radio" id="nodes-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="nodes-true" name="value" value="true" />
{/if}
<label for="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="false">Continuous scroll iteratively adding 10 nodes.</label>
</div>
</div>
<Button color="primary" type="submit">Submit</Button>
</form>
</Card>
</Col>
</Row>

View File

@ -138,6 +138,7 @@
export let numaccs = 0; export let numaccs = 0;
export let zoomState = null; export let zoomState = null;
export let thresholdState = null; export let thresholdState = null;
export let extendedLegendData = null;
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true; if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
@ -191,6 +192,7 @@
className && legendEl.classList.add(className); className && legendEl.classList.add(className);
uPlot.assign(legendEl.style, { uPlot.assign(legendEl.style, {
minWidth: extendedLegendData ? "300px" : "100px",
textAlign: "left", textAlign: "left",
pointerEvents: "none", pointerEvents: "none",
display: "none", display: "none",
@ -307,13 +309,6 @@
} }
} }
const plotSeries = [
{
label: "Runtime",
value: (u, ts, sidx, didx) =>
didx == null ? null : formatTime(ts, forNode),
},
];
const plotData = [new Array(longestSeries)]; const plotData = [new Array(longestSeries)];
if (forNode === true) { if (forNode === true) {
// Negative Timestamp Buildup // Negative Timestamp Buildup
@ -330,6 +325,15 @@
plotData[0][j] = j * timestep; plotData[0][j] = j * timestep;
} }
const plotSeries = [
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
{
label: "Runtime",
value: (u, ts, sidx, didx) =>
(didx == null) ? null : formatTime(ts, forNode),
}
];
let plotBands = undefined; let plotBands = undefined;
if (useStatsSeries) { if (useStatsSeries) {
plotData.push(statisticsSeries.min); plotData.push(statisticsSeries.min);
@ -366,16 +370,61 @@
} else { } else {
for (let i = 0; i < series.length; i++) { for (let i = 0; i < series.length; i++) {
plotData.push(series[i].data); plotData.push(series[i].data);
// Default
if (!extendedLegendData) {
plotSeries.push({ plotSeries.push({
label: label:
scope === "node" scope === "node"
? series[i].hostname ? series[i].hostname
: scope === "accelerator"
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
: scope + " #" + (i + 1), : scope + " #" + (i + 1),
scale: "y", scale: "y",
width: lineWidth, width: lineWidth,
stroke: lineColor(i, series.length), stroke: lineColor(i, series.length),
}); });
} }
// Extended Legend For NodeList
else {
plotSeries.push({
label:
scope === "node"
? series[i].hostname
: scope === "accelerator"
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
: scope + " #" + (i + 1),
scale: "y",
width: lineWidth,
stroke: lineColor(i, series.length),
values: (u, sidx, idx) => {
// "i" = "sidx - 1" : sidx contains x-axis-data
if (idx == null)
return {
time: '-',
value: '-',
user: '-',
job: '-'
};
if (series[i].id in extendedLegendData) {
return {
time: formatTime(plotData[0][idx], forNode),
value: plotData[sidx][idx],
user: extendedLegendData[series[i].id].user,
job: extendedLegendData[series[i].id].job,
};
} else {
return {
time: formatTime(plotData[0][idx], forNode),
value: plotData[sidx][idx],
user: '-',
job: '-',
};
}
}
});
}
}
} }
const opts = { const opts = {

View File

@ -9,7 +9,6 @@
<script> <script>
import { import {
Spinner,
Icon, Icon,
Button, Button,
Card, Card,
@ -18,61 +17,24 @@
Input, Input,
InputGroup, InputGroup,
InputGroupText, } from "@sveltestrap/sveltestrap"; InputGroupText, } from "@sveltestrap/sveltestrap";
import {
queryStore,
gql,
getContextClient,
} from "@urql/svelte";
export let cluster; export let cluster;
export let subCluster export let subCluster
export let hostname; export let hostname;
export let dataHealth; export let dataHealth;
export let nodeJobsData = null;
const client = getContextClient();
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`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
items {
user
project
exclusive
}
count
}
}
`;
// Not at least one returned, selected metric: NodeHealth warning // Not at least one returned, selected metric: NodeHealth warning
const healthWarn = !dataHealth.includes(true); const healthWarn = !dataHealth.includes(true);
// At least one non-returned selected metric: Metric config error? // At least one non-returned selected metric: Metric config error?
const metricWarn = dataHealth.includes(false); const metricWarn = dataHealth.includes(false);
$: nodeJobsData = queryStore({
client: client,
query: nodeJobsQuery,
variables: { paging, sorting, filter },
});
let userList; let userList;
let projectList; let projectList;
$: if ($nodeJobsData?.data) { $: if (nodeJobsData) {
userList = Array.from(new Set($nodeJobsData.data.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b)); userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b));
projectList = Array.from(new Set($nodeJobsData.data.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b)); projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b));
} }
</script> </script>
<Card class="pb-3"> <Card class="pb-3">
@ -92,9 +54,6 @@
</div> </div>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
{#if $nodeJobsData.fetching}
<Spinner />
{:else if $nodeJobsData.data}
{#if healthWarn} {#if healthWarn}
<InputGroup> <InputGroup>
<InputGroupText> <InputGroupText>
@ -119,7 +78,7 @@
Missing Metric Missing Metric
</Button> </Button>
</InputGroup> </InputGroup>
{:else if $nodeJobsData.data.jobs.count == 1 && $nodeJobsData.data.jobs.items[0].exclusive} {:else if nodeJobsData.jobs.count == 1 && nodeJobsData.jobs.items[0].exclusive}
<InputGroup> <InputGroup>
<InputGroupText> <InputGroupText>
<Icon name="circle-fill"/> <Icon name="circle-fill"/>
@ -131,7 +90,7 @@
Exclusive Exclusive
</Button> </Button>
</InputGroup> </InputGroup>
{:else if $nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive} {:else if nodeJobsData.jobs.count >= 1 && !nodeJobsData.jobs.items[0].exclusive}
<InputGroup> <InputGroup>
<InputGroupText> <InputGroupText>
<Icon name="circle-half"/> <Icon name="circle-half"/>
@ -165,7 +124,7 @@
<InputGroupText class="justify-content-center" style="width: 4.4rem;"> <InputGroupText class="justify-content-center" style="width: 4.4rem;">
Activity Activity
</InputGroupText> </InputGroupText>
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{$nodeJobsData?.data?.jobs?.count || 0} Job{($nodeJobsData?.data?.jobs?.count == 1) ? '': 's'}" disabled /> <Input class="flex-grow-1" style="background-color: white;" type="text" value="{nodeJobsData?.jobs?.count || 0} Job{(nodeJobsData?.jobs?.count == 1) ? '': 's'}" disabled />
<a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" > <a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
<Icon name="view-list" /> <Icon name="view-list" />
List List
@ -213,7 +172,6 @@
</div> </div>
</Card> </Card>
{/if} {/if}
{/if}
</CardBody> </CardBody>
</Card> </Card>

View File

@ -8,7 +8,12 @@
--> -->
<script> <script>
import { Card } from "@sveltestrap/sveltestrap"; import {
queryStore,
gql,
getContextClient,
} from "@urql/svelte";
import { Card, CardBody, Spinner } from "@sveltestrap/sveltestrap";
import { maxScope, checkMetricDisabled } from "../../generic/utils.js"; import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
import MetricPlot from "../../generic/plots/MetricPlot.svelte"; import MetricPlot from "../../generic/plots/MetricPlot.svelte";
import NodeInfo from "./NodeInfo.svelte"; import NodeInfo from "./NodeInfo.svelte";
@ -17,6 +22,43 @@
export let nodeData; export let nodeData;
export let selectedMetrics; export let selectedMetrics;
const client = getContextClient();
const paging = { itemsPerPage: 50, page: 1 };
const sorting = { field: "startTime", type: "col", order: "DESC" };
const filter = [
{ cluster: { eq: cluster } },
{ node: { contains: nodeData.host } },
{ state: ["running"] },
];
const nodeJobsQuery = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
items {
jobId
user
project
exclusive
resources {
hostname
accelerators
}
}
count
}
}
`;
$: nodeJobsData = queryStore({
client: client,
query: nodeJobsQuery,
variables: { paging, sorting, filter },
});
// Helper // Helper
const selectScope = (nodeMetrics) => const selectScope = (nodeMetrics) =>
nodeMetrics.reduce( nodeMetrics.reduce(
@ -51,14 +93,44 @@
let dataHealth; let dataHealth;
$: if (nodeData?.metrics) { $: if (nodeData?.metrics) {
refinedData = sortAndSelectScope(nodeData?.metrics) refinedData = sortAndSelectScope(nodeData?.metrics)
// Check data for series, skip disabled
dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0)) dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0))
} }
let extendedLegendData = null;
$: if ($nodeJobsData?.data) {
// Get Shared State of Node: Only Build extended Legend For Shared Nodes
if ($nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive) {
const accSet = Array.from(new Set($nodeJobsData.data.jobs.items
.map((i) => i.resources
.filter((r) => r.hostname === nodeData.host)
.map((r) => r.accelerators)
)
)).flat(2)
extendedLegendData = {}
for (const accId of accSet) {
const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId)))
extendedLegendData[accId] = {
user: matchJob?.user ? matchJob?.user : '-',
job: matchJob?.jobId ? matchJob?.jobId : '-',
}
}
// Theoretically extendable for hwthreadIDs
}
}
</script> </script>
<tr> <tr>
<td> <td>
<NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} {dataHealth}/> {#if $nodeJobsData.fetching}
<Card>
<CardBody class="content-center">
<Spinner/>
</CardBody>
</Card>
{:else}
<NodeInfo nodeJobsData={$nodeJobsData.data} {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} {dataHealth}/>
{/if}
</td> </td>
{#each refinedData as metricData (metricData.data.name)} {#each refinedData as metricData (metricData.data.name)}
<td> <td>
@ -83,6 +155,7 @@
forNode forNode
/> />
<div class="my-2"/> <div class="my-2"/>
{#key extendedLegendData}
<MetricPlot <MetricPlot
{cluster} {cluster}
subCluster={nodeData.subCluster} subCluster={nodeData.subCluster}
@ -91,8 +164,10 @@
timestep={metricData.data.metric.timestep} timestep={metricData.data.metric.timestep}
series={metricData.data.metric.series} series={metricData.data.metric.series}
height={175} height={175}
{extendedLegendData}
forNode forNode
/> />
{/key}
{:else} {:else}
<MetricPlot <MetricPlot
{cluster} {cluster}

View File

@ -8,6 +8,7 @@
{{define "javascript"}} {{define "javascript"}}
<script> <script>
const isAdmin = {{ .User.HasRole .Roles.admin }}; const isAdmin = {{ .User.HasRole .Roles.admin }};
const isSupport = {{ .User.HasRole .Roles.support }};
const isApi = {{ .User.HasRole .Roles.api }}; const isApi = {{ .User.HasRole .Roles.api }};
const username = {{ .User.Username }}; const username = {{ .User.Username }};
const filterPresets = {{ .FilterPresets }}; const filterPresets = {{ .FilterPresets }};