mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-11-20 08:47:22 +01:00
finalize timed node state frontend code for status view
This commit is contained in:
@@ -1,34 +1,25 @@
|
|||||||
<!--
|
<!--
|
||||||
@component Node State/Health Data Stacked Plot Component, based on uPlot; states by timestamp
|
@component Node State/Health Data Stacked Plot Component, based on uPlot; states by timestamp
|
||||||
|
|
||||||
Only width/height should change reactively.
|
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `metric String?`: The metric name [Default: ""]
|
|
||||||
- `width Number?`: The plot width [Default: 0]
|
- `width Number?`: The plot width [Default: 0]
|
||||||
- `height Number?`: The plot height [Default: 300]
|
- `height Number?`: The plot height [Default: 300]
|
||||||
- `data [Array]`: The data object [Default: null]
|
- `data [Array]`: The data object [Default: null]
|
||||||
- `title String?`: Plot title [Default: ""]
|
|
||||||
- `xlabel String?`: Plot X axis label [Default: ""]
|
- `xlabel String?`: Plot X axis label [Default: ""]
|
||||||
- `ylabel String?`: Plot Y axis label [Default: ""]
|
- `ylabel String?`: Plot Y axis label [Default: ""]
|
||||||
- `yunit String?`: Plot Y axis unit [Default: ""]
|
- `yunit String?`: Plot Y axis unit [Default: ""]
|
||||||
- `xticks Array`: Array containing jobIDs [Default: []]
|
- `title String?`: Plot title [Default: ""]
|
||||||
- `xinfo Array`: Array containing job information [Default: []]
|
- `stateType String?`: Which states to render, affects plot render config [Options: Health, Node; Default: ""]
|
||||||
- `forResources Bool?`: Render this plot for allocated jobResources [Default: false]
|
|
||||||
- `plot Sync Object!`: uPlot cursor synchronization key
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import uPlot from "uplot";
|
import uPlot from "uplot";
|
||||||
import { roundTwoDigits, formatDurationTime, formatUnixTime, formatNumber } from "../units.js";
|
import { formatUnixTime } from "../units.js";
|
||||||
import { getContext, onMount, onDestroy } from "svelte";
|
import { getContext, onMount, onDestroy } from "svelte";
|
||||||
import { Card } from "@sveltestrap/sveltestrap";
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
|
|
||||||
|
|
||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
cluster = "",
|
|
||||||
width = 0,
|
width = 0,
|
||||||
height = 300,
|
height = 300,
|
||||||
data = null,
|
data = null,
|
||||||
@@ -36,16 +27,86 @@
|
|||||||
ylabel = "",
|
ylabel = "",
|
||||||
yunit = "",
|
yunit = "",
|
||||||
title = "",
|
title = "",
|
||||||
stateType = "" // Health, Slurm, Both
|
stateType = "" // Health, Node
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
/* Const Init */
|
/* Const Init */
|
||||||
const clusterCockpitConfig = getContext("cc-config");
|
const clusterCockpitConfig = getContext("cc-config");
|
||||||
const lineWidth = clusterCockpitConfig?.plotConfiguration_lineWidth / window.devicePixelRatio || 2;
|
const lineWidth = clusterCockpitConfig?.plotConfiguration_lineWidth / window.devicePixelRatio || 2;
|
||||||
const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
|
const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
|
||||||
|
const seriesConfig = {
|
||||||
|
full: {
|
||||||
|
label: "Full",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: cbmode ? "rgba(0, 110, 0, 0.4)" : "rgba(0, 128, 0, 0.4)",
|
||||||
|
stroke: cbmode ? "rgb(0, 110, 0)" : "green",
|
||||||
|
},
|
||||||
|
partial: {
|
||||||
|
label: "Partial",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: cbmode ? "rgba(235, 172, 35, 0.4)" : "rgba(255, 215, 0, 0.4)",
|
||||||
|
stroke: cbmode ? "rgb(235, 172, 35)" : "gold",
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
label: "Failed",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: cbmode ? "rgb(181, 29, 20, 0.4)" : "rgba(255, 0, 0, 0.4)",
|
||||||
|
stroke: cbmode ? "rgb(181, 29, 20)" : "red",
|
||||||
|
},
|
||||||
|
idle: {
|
||||||
|
label: "Idle",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: cbmode ? "rgba(0, 140, 249, 0.4)" : "rgba(0, 0, 255, 0.4)",
|
||||||
|
stroke: cbmode ? "rgb(0, 140, 249)" : "blue",
|
||||||
|
},
|
||||||
|
allocated: {
|
||||||
|
label: "Allocated",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: cbmode ? "rgba(0, 110, 0, 0.4)" : "rgba(0, 128, 0, 0.4)",
|
||||||
|
stroke: cbmode ? "rgb(0, 110, 0)" : "green",
|
||||||
|
},
|
||||||
|
reserved: {
|
||||||
|
label: "Reserved",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: cbmode ? "rgba(209, 99, 230, 0.4)" : "rgba(255, 0, 255, 0.4)",
|
||||||
|
stroke: cbmode ? "rgb(209, 99, 230)" : "magenta",
|
||||||
|
},
|
||||||
|
mixed: {
|
||||||
|
label: "Mixed",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: cbmode ? "rgba(235, 172, 35, 0.4)" : "rgba(255, 215, 0, 0.4)",
|
||||||
|
stroke: cbmode ? "rgb(235, 172, 35)" : "gold",
|
||||||
|
},
|
||||||
|
down: {
|
||||||
|
label: "Down",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: cbmode ? "rgba(181, 29 ,20, 0.4)" : "rgba(255, 0, 0, 0.4)",
|
||||||
|
stroke: cbmode ? "rgb(181, 29, 20)" : "red",
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
label: "Unknown",
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
fill: "rgba(0, 0, 0, 0.4)",
|
||||||
|
stroke: "black",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Data Prep For uPlot
|
||||||
|
const sortedData = data.sort((a, b) => a.state.localeCompare(b.state));
|
||||||
|
const collectLabel = sortedData.map(d => d.state);
|
||||||
|
// Align Data to Timesteps, Introduces 'undefied' as placeholder, reiterate and set those to 0
|
||||||
|
const collectData = uPlot.join(sortedData.map(d => [d.times, d.counts])).map(d => d.map(i => i ? i : 0));
|
||||||
|
|
||||||
// STACKED CHART FUNCTIONS //
|
// STACKED CHART FUNCTIONS //
|
||||||
|
|
||||||
function stack(data, omit) {
|
function stack(data, omit) {
|
||||||
let data2 = [];
|
let data2 = [];
|
||||||
let bands = [];
|
let bands = [];
|
||||||
@@ -74,23 +135,46 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOpts(title, series) {
|
function getStackedOpts(title, width, height, series, data) {
|
||||||
return {
|
let opts = {
|
||||||
scales: {
|
width,
|
||||||
x: {
|
height,
|
||||||
time: false,
|
title,
|
||||||
|
plugins: [legendAsTooltipPlugin()],
|
||||||
|
series,
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
scale: "x",
|
||||||
|
space: 25, // Tick Spacing
|
||||||
|
rotate: 30,
|
||||||
|
show: true,
|
||||||
|
label: xlabel,
|
||||||
|
values(self, splits) {
|
||||||
|
return splits.map(s => formatUnixTime(s));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scale: "y",
|
||||||
|
grid: { show: true },
|
||||||
|
labelFont: "sans-serif",
|
||||||
|
label: ylabel + (yunit ? ` (${yunit})` : ''),
|
||||||
|
// values: (u, vals) => vals.map((v) => formatNumber(v)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
padding: [5, 10, 0, 0],
|
||||||
|
scales: {
|
||||||
|
x: { time: false },
|
||||||
|
y: { auto: true, distr: 1 },
|
||||||
},
|
},
|
||||||
series
|
legend: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
drag: { x: true, y: true },
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function getStackedOpts(title, series, data, interp) {
|
let stacked = stack(data, i => false);
|
||||||
let opts = getOpts(title, series);
|
|
||||||
|
|
||||||
let interped = interp ? interp(data) : data;
|
|
||||||
|
|
||||||
let stacked = stack(interped, i => false);
|
|
||||||
opts.bands = stacked.bands;
|
opts.bands = stacked.bands;
|
||||||
|
|
||||||
opts.cursor = opts.cursor || {};
|
opts.cursor = opts.cursor || {};
|
||||||
@@ -99,7 +183,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
opts.series.forEach(s => {
|
opts.series.forEach(s => {
|
||||||
s.value = (u, v, si, i) => data[si][i];
|
// Format Time Info from Unix TS to LocalTimeString
|
||||||
|
s.value = (u, v, si, i) => (si === 0) ? formatUnixTime(data[si][i]) : data[si][i];
|
||||||
|
|
||||||
s.points = s.points || {};
|
s.points = s.points || {};
|
||||||
|
|
||||||
@@ -138,331 +223,7 @@
|
|||||||
return {opts, data: stacked.data};
|
return {opts, data: stacked.data};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UPLOT PLUGIN: Converts the legend into a simple tooltip
|
||||||
function stack2(series) {
|
|
||||||
// for uplot data
|
|
||||||
let data = Array(series.length);
|
|
||||||
let bands = [];
|
|
||||||
|
|
||||||
let dataLen = series[0].values.length;
|
|
||||||
|
|
||||||
let zeroArr = Array(dataLen).fill(0);
|
|
||||||
|
|
||||||
let stackGroups = new Map();
|
|
||||||
let seriesStackKeys = Array(series.length);
|
|
||||||
|
|
||||||
series.forEach((s, si) => {
|
|
||||||
let vals = s.values.slice();
|
|
||||||
|
|
||||||
// apply negY
|
|
||||||
if (s.negY) {
|
|
||||||
for (let i = 0; i < vals.length; i++) {
|
|
||||||
if (vals[i] != null)
|
|
||||||
vals[i] *= -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.stacking.mode != 'none') {
|
|
||||||
let hasPos = vals.some(v => v > 0);
|
|
||||||
// derive stacking key
|
|
||||||
let stackKey = seriesStackKeys[si] = s.stacking.mode + s.scaleKey + s.stacking.group + (hasPos ? '+' : '-');
|
|
||||||
let group = stackGroups.get(stackKey);
|
|
||||||
|
|
||||||
// initialize stacking group
|
|
||||||
if (group == null) {
|
|
||||||
group = {
|
|
||||||
series: [],
|
|
||||||
acc: zeroArr.slice(),
|
|
||||||
dir: hasPos ? -1 : 1,
|
|
||||||
};
|
|
||||||
stackGroups.set(stackKey, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
// push for bands gen
|
|
||||||
group.series.unshift(si);
|
|
||||||
|
|
||||||
let stacked = data[si] = Array(dataLen);
|
|
||||||
let { acc } = group;
|
|
||||||
|
|
||||||
for (let i = 0; i < dataLen; i++) {
|
|
||||||
let v = vals[i];
|
|
||||||
|
|
||||||
if (v != null)
|
|
||||||
stacked[i] = (acc[i] += v);
|
|
||||||
else
|
|
||||||
stacked[i] = v; // we may want to coerce to 0 here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
data[si] = vals;
|
|
||||||
});
|
|
||||||
|
|
||||||
// re-compute by percent
|
|
||||||
series.forEach((s, si) => {
|
|
||||||
if (s.stacking.mode == 'percent') {
|
|
||||||
let group = stackGroups.get(seriesStackKeys[si]);
|
|
||||||
let { acc } = group;
|
|
||||||
|
|
||||||
// re-negatify percent
|
|
||||||
let sign = group.dir * -1;
|
|
||||||
|
|
||||||
let stacked = data[si];
|
|
||||||
|
|
||||||
for (let i = 0; i < dataLen; i++) {
|
|
||||||
let v = stacked[i];
|
|
||||||
|
|
||||||
if (v != null)
|
|
||||||
stacked[i] = sign * (v / acc[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// generate bands between adjacent group series
|
|
||||||
stackGroups.forEach(group => {
|
|
||||||
let { series, dir } = group;
|
|
||||||
let lastIdx = series.length - 1;
|
|
||||||
|
|
||||||
series.forEach((si, i) => {
|
|
||||||
if (i != lastIdx) {
|
|
||||||
let nextIdx = series[i + 1];
|
|
||||||
bands.push({
|
|
||||||
// since we're not passing x series[0] for stacking, real idxs are actually +1
|
|
||||||
series: [si + 1, nextIdx + 1],
|
|
||||||
dir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
bands,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// UPLOT SERIES INIT //
|
|
||||||
|
|
||||||
const plotSeries = [
|
|
||||||
{
|
|
||||||
label: "Time",
|
|
||||||
scale: "x",
|
|
||||||
value: (u, ts, sidx, didx) =>
|
|
||||||
(didx == null) ? null : formatUnixTime(ts),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
if (stateType === "slurm") {
|
|
||||||
const resSeries = [
|
|
||||||
{
|
|
||||||
label: "Idle",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(136, 204, 238)" : "lightblue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Allocated",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(30, 136, 229)" : "green",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Reserved",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(211, 95, 183)" : "magenta",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Mixed",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(239, 230, 69)" : "yellow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Down",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(225, 86, 44)" : "red",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Unknown",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: "black",
|
|
||||||
}
|
|
||||||
];
|
|
||||||
plotSeries.push(...resSeries)
|
|
||||||
} else if (stateType === "health") {
|
|
||||||
const resSeries = [
|
|
||||||
{
|
|
||||||
label: "Full",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(30, 136, 229)" : "green",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Partial",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(239, 230, 69)" : "yellow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Failed",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(225, 86, 44)" : "red",
|
|
||||||
}
|
|
||||||
];
|
|
||||||
plotSeries.push(...resSeries)
|
|
||||||
} else {
|
|
||||||
const resSeries = [
|
|
||||||
{
|
|
||||||
label: "Full",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(30, 136, 229)" : "green",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Partial",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(239, 230, 69)" : "yellow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Failed",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(225, 86, 44)" : "red",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Idle",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(136, 204, 238)" : "lightblue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Allocated",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(30, 136, 229)" : "green",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Reserved",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(211, 95, 183)" : "magenta",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Mixed",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(239, 230, 69)" : "yellow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Down",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: cbmode ? "rgb(225, 86, 44)" : "red",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Unknown",
|
|
||||||
scale: "y",
|
|
||||||
width: lineWidth,
|
|
||||||
stroke: "black",
|
|
||||||
}
|
|
||||||
];
|
|
||||||
plotSeries.push(...resSeries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UPLOT BAND COLORS //
|
|
||||||
// const plotBands = [
|
|
||||||
// { 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)" },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// UPLOT OPTIONS //
|
|
||||||
const opts = {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
title,
|
|
||||||
plugins: [legendAsTooltipPlugin()],
|
|
||||||
series: plotSeries,
|
|
||||||
axes: [
|
|
||||||
{
|
|
||||||
scale: "x",
|
|
||||||
space: 25, // Tick Spacing
|
|
||||||
rotate: 30,
|
|
||||||
show: true,
|
|
||||||
label: xlabel,
|
|
||||||
// values(self, splits) {
|
|
||||||
// return splits.map(s => xticks[s]);
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scale: "y",
|
|
||||||
grid: { show: true },
|
|
||||||
labelFont: "sans-serif",
|
|
||||||
label: ylabel + (yunit ? ` (${yunit})` : ''),
|
|
||||||
// values: (u, vals) => vals.map((v) => formatNumber(v)),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// bands: forResources ? [] : plotBands,
|
|
||||||
padding: [5, 10, 0, 0],
|
|
||||||
// hooks: {
|
|
||||||
// draw: [
|
|
||||||
// (u) => {
|
|
||||||
// // Draw plot type label:
|
|
||||||
// let textl = forResources ? "Job Resources by Type" : "Metric Min/Avg/Max for Job Duration";
|
|
||||||
// let textr = "Earlier <- StartTime -> Later";
|
|
||||||
// u.ctx.save();
|
|
||||||
// u.ctx.textAlign = "start";
|
|
||||||
// u.ctx.fillStyle = "black";
|
|
||||||
// u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10);
|
|
||||||
// u.ctx.textAlign = "end";
|
|
||||||
// u.ctx.fillStyle = "black";
|
|
||||||
// u.ctx.fillText(
|
|
||||||
// textr,
|
|
||||||
// u.bbox.left + u.bbox.width - 10,
|
|
||||||
// u.bbox.top + 10,
|
|
||||||
// );
|
|
||||||
// u.ctx.restore();
|
|
||||||
// return;
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
scales: {
|
|
||||||
x: { time: false },
|
|
||||||
y: {auto: true, distr: 1},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
// Display legend
|
|
||||||
show: true,
|
|
||||||
live: true,
|
|
||||||
},
|
|
||||||
cursor: {
|
|
||||||
drag: { x: true, y: true },
|
|
||||||
// sync: {
|
|
||||||
// key: plotSync.key,
|
|
||||||
// scales: ["x", null],
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Var Init */
|
|
||||||
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({
|
function legendAsTooltipPlugin({
|
||||||
className,
|
className,
|
||||||
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||||
@@ -476,7 +237,7 @@
|
|||||||
className && legendEl.classList.add(className);
|
className && legendEl.classList.add(className);
|
||||||
|
|
||||||
uPlot.assign(legendEl.style, {
|
uPlot.assign(legendEl.style, {
|
||||||
minWidth: "100px",
|
minWidth: "175px",
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
display: "none",
|
display: "none",
|
||||||
@@ -489,9 +250,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// hide series color markers:
|
// hide series color markers:
|
||||||
const idents = legendEl.querySelectorAll(".u-marker");
|
// const idents = legendEl.querySelectorAll(".u-marker");
|
||||||
for (let i = 0; i < idents.length; i++)
|
// for (let i = 0; i < idents.length; i++)
|
||||||
idents[i].style.display = "none";
|
// idents[i].style.display = "none";
|
||||||
|
|
||||||
const overEl = u.over;
|
const overEl = u.over;
|
||||||
overEl.style.overflow = "visible";
|
overEl.style.overflow = "visible";
|
||||||
@@ -523,13 +284,34 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// RENDER HANDLING
|
// UPLOT SERIES INIT
|
||||||
|
const plotSeries = [
|
||||||
|
{
|
||||||
|
label: "Time",
|
||||||
|
scale: "x"
|
||||||
|
},
|
||||||
|
...collectLabel.map(l => seriesConfig[l])
|
||||||
|
]
|
||||||
|
|
||||||
|
/* Var Init */
|
||||||
|
let timeoutId = null;
|
||||||
|
let uplot = null;
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let plotWrapper = $state(null);
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
$effect(() => {
|
||||||
|
if (plotWrapper) {
|
||||||
|
onSizeChange(width, height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function render(ren_width, ren_height) {
|
function render(ren_width, ren_height) {
|
||||||
if (!uplot) {
|
if (!uplot) {
|
||||||
opts.width = ren_width;
|
let { opts, data } = getStackedOpts(title, ren_width, ren_height, plotSeries, collectData);
|
||||||
opts.height = ren_height;
|
uplot = new uPlot(opts, data, plotWrapper); // Data is uplot formatted [[X][Y1][Y2]...]
|
||||||
uplot = new uPlot(opts, data, plotWrapper); // Data is uplot formatted [[X][Ymin][Yavg][Ymax]]
|
|
||||||
plotSync.sub(uplot)
|
|
||||||
} else {
|
} else {
|
||||||
uplot.setSize({ width: ren_width, height: ren_height });
|
uplot.setSize({ width: ren_width, height: ren_height });
|
||||||
}
|
}
|
||||||
@@ -559,12 +341,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Define $width Wrapper and NoData Card -->
|
<!-- Define $width Wrapper and NoData Card -->
|
||||||
{#if data && data[0].length > 0}
|
{#if data && collectData[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>
|
></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>{stateType} State Stacked Chart</code></Card
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -51,12 +51,13 @@ export function formatDurationTime(t, forNode = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatUnixTime(t) {
|
export function formatUnixTime(t, withDate = false) {
|
||||||
if (t !== null) {
|
if (t !== null) {
|
||||||
if (isNaN(t)) {
|
if (isNaN(t)) {
|
||||||
return t;
|
return t;
|
||||||
} else {
|
} else {
|
||||||
return new Date(t * 1000).toLocaleString()
|
if (withDate) return new Date(t * 1000).toLocaleString();
|
||||||
|
else return new Date(t * 1000).toLocaleTimeString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,12 @@
|
|||||||
import {
|
import {
|
||||||
init,
|
init,
|
||||||
} from "../generic/utils.js";
|
} from "../generic/utils.js";
|
||||||
import { scaleNumbers, formatDurationTime } from "../generic/units.js";
|
import { formatDurationTime } from "../generic/units.js";
|
||||||
import Refresher from "../generic/helper/Refresher.svelte";
|
import Refresher from "../generic/helper/Refresher.svelte";
|
||||||
|
import TimeSelection from "../generic/select/TimeSelection.svelte";
|
||||||
import Roofline from "../generic/plots/Roofline.svelte";
|
import Roofline from "../generic/plots/Roofline.svelte";
|
||||||
import Pie, { colors } from "../generic/plots/Pie.svelte";
|
import Pie, { colors } from "../generic/plots/Pie.svelte";
|
||||||
|
import Stacked from "../generic/plots/Stacked.svelte";
|
||||||
|
|
||||||
/* Svelte 5 Props */
|
/* Svelte 5 Props */
|
||||||
let {
|
let {
|
||||||
@@ -44,10 +46,12 @@
|
|||||||
/* State Init */
|
/* State Init */
|
||||||
let cluster = $state(presetCluster);
|
let cluster = $state(presetCluster);
|
||||||
let pieWidth = $state(0);
|
let pieWidth = $state(0);
|
||||||
let stackedWidth = $state(0);
|
let stackedWidth1 = $state(0);
|
||||||
|
let stackedWidth2 = $state(0);
|
||||||
let plotWidths = $state([]);
|
let plotWidths = $state([]);
|
||||||
let from = $state(new Date(Date.now() - 5 * 60 * 1000));
|
let from = $state(new Date(Date.now() - 5 * 60 * 1000));
|
||||||
let to = $state(new Date(Date.now()));
|
let to = $state(new Date(Date.now()));
|
||||||
|
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
|
||||||
// Bar Gauges
|
// Bar Gauges
|
||||||
let allocatedNodes = $state({});
|
let allocatedNodes = $state({});
|
||||||
let allocatedAccs = $state({});
|
let allocatedAccs = $state({});
|
||||||
@@ -80,28 +84,38 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const refinedStateData = $derived.by(() => {
|
const refinedStateData = $derived.by(() => {
|
||||||
return $nodesStateCounts?.data?.nodeStates.filter((e) => ['allocated', 'reserved', 'idle', 'mixed','down', 'unknown'].includes(e.state))
|
return $nodesStateCounts?.data?.nodeStates.
|
||||||
|
filter((e) => ['allocated', 'reserved', 'idle', 'mixed','down', 'unknown'].includes(e.state)).
|
||||||
|
sort((a, b) => b.count - a.count)
|
||||||
});
|
});
|
||||||
|
|
||||||
const refinedHealthData = $derived.by(() => {
|
const refinedHealthData = $derived.by(() => {
|
||||||
return $nodesStateCounts?.data?.nodeStates.filter((e) => ['full', 'partial', 'failed'].includes(e.state))
|
return $nodesStateCounts?.data?.nodeStates.
|
||||||
|
filter((e) => ['full', 'partial', 'failed'].includes(e.state)).
|
||||||
|
sort((a, b) => b.count - a.count)
|
||||||
});
|
});
|
||||||
|
|
||||||
// NodeStates for Stacked charts
|
// States for Stacked charts
|
||||||
const nodesStateTimes = $derived(queryStore({
|
const statesTimed = $derived(queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query ($filter: [NodeFilter!]) {
|
query ($filter: [NodeFilter!], $typeNode: String!, $typeHealth: String!) {
|
||||||
nodeStatesTimed(filter: $filter) {
|
nodeStates: nodeStatesTimed(filter: $filter, type: $typeNode) {
|
||||||
state
|
state
|
||||||
type
|
counts
|
||||||
count
|
times
|
||||||
time
|
}
|
||||||
|
healthStates: nodeStatesTimed(filter: $filter, type: $typeHealth) {
|
||||||
|
state
|
||||||
|
counts
|
||||||
|
times
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
variables: {
|
variables: {
|
||||||
filter: { cluster: { eq: cluster }, timeStart: Date.now() - (24 * 3600 * 1000)} // Add Selector for Timeframe (4h, 12h, 24h)?
|
filter: { cluster: { eq: cluster }, timeStart: 1760097059}, // stackedFrom
|
||||||
|
typeNode: "node",
|
||||||
|
typeHealth: "health"
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -378,12 +392,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Refresher and space for other options -->
|
<!-- Refresher and space for other options -->
|
||||||
<Row class="justify-content-end">
|
<Row class="justify-content-between">
|
||||||
|
<Col xs="12" md="5" lg="4" xl="3">
|
||||||
|
<TimeSelection
|
||||||
|
customEnabled={false}
|
||||||
|
applyTime={(newFrom, newTo) => {
|
||||||
|
stackedFrom = Math.floor(newFrom.getTime() / 1000);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
<Col xs="12" md="5" lg="4" xl="3">
|
<Col xs="12" md="5" lg="4" xl="3">
|
||||||
<Refresher
|
<Refresher
|
||||||
initially={120}
|
initially={120}
|
||||||
onRefresh={() => {
|
onRefresh={() => {
|
||||||
console.log('Trigger Refresh StatusTab')
|
|
||||||
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());
|
||||||
}}
|
}}
|
||||||
@@ -394,43 +415,40 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<!-- Node Stack Charts Dev-->
|
<!-- Node Stack Charts Dev-->
|
||||||
<!--
|
{#if $initq.data && $statesTimed.data}
|
||||||
{#if $initq.data && $nodesStateTimes.data}
|
<Row cols={{ md: 2 , sm: 1}} class="mb-3 justify-content-center">
|
||||||
<Row cols={{ lg: 4, md: 2 , sm: 1}} class="mb-3 justify-content-center">
|
|
||||||
<Col class="px-3 mt-2 mt-lg-0">
|
<Col class="px-3 mt-2 mt-lg-0">
|
||||||
<div bind:clientWidth={stackedWidth}>
|
<div bind:clientWidth={stackedWidth1}>
|
||||||
{#key $nodesStateTimes.data}
|
{#key $statesTimed?.data?.nodeStates}
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States Over Time
|
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States Over Time
|
||||||
</h4>
|
</h4>
|
||||||
<Stacked
|
<Stacked
|
||||||
{cluster}
|
data={$statesTimed?.data?.nodeStates}
|
||||||
data={$nodesStateTimes?.data}
|
width={stackedWidth1 * 0.95}
|
||||||
width={stackedWidth * 0.55}
|
|
||||||
xLabel="Time"
|
xLabel="Time"
|
||||||
yLabel="Nodes"
|
yLabel="Nodes"
|
||||||
yunit = "#Count"
|
yunit = "#Count"
|
||||||
title = "Slurm States"
|
title = "Node States"
|
||||||
stateType = "slurm"
|
stateType = "Node"
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="px-3 mt-2 mt-lg-0">
|
<Col class="px-3 mt-2 mt-lg-0">
|
||||||
<div bind:clientWidth={stackedWidth}>
|
<div bind:clientWidth={stackedWidth2}>
|
||||||
{#key $nodesStateTimes.data}
|
{#key $statesTimed?.data?.healthStates}
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Health States Over Time
|
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Health States Over Time
|
||||||
</h4>
|
</h4>
|
||||||
<Stacked
|
<Stacked
|
||||||
{cluster}
|
data={$statesTimed?.data?.healthStates}
|
||||||
data={$nodesStateTimes?.data}
|
width={stackedWidth2 * 0.95}
|
||||||
width={stackedWidth * 0.55}
|
|
||||||
xLabel="Time"
|
xLabel="Time"
|
||||||
yLabel="Nodes"
|
yLabel="Nodes"
|
||||||
yunit = "#Count"
|
yunit = "#Count"
|
||||||
title = "Health States"
|
title = "Health States"
|
||||||
stateType = "health"
|
stateType = "Health"
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
@@ -439,8 +457,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
<hr/>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Node Health Pis, later Charts -->
|
<!-- Node Health Pis, later Charts -->
|
||||||
{#if $initq.data && $nodesStateCounts.data}
|
{#if $initq.data && $nodesStateCounts.data}
|
||||||
@@ -449,7 +465,7 @@
|
|||||||
<div bind:clientWidth={pieWidth}>
|
<div bind:clientWidth={pieWidth}>
|
||||||
{#key refinedStateData}
|
{#key refinedStateData}
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States
|
Current {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States
|
||||||
</h4>
|
</h4>
|
||||||
<Pie
|
<Pie
|
||||||
{useAltColors}
|
{useAltColors}
|
||||||
@@ -489,7 +505,7 @@
|
|||||||
<div bind:clientWidth={pieWidth}>
|
<div bind:clientWidth={pieWidth}>
|
||||||
{#key refinedHealthData}
|
{#key refinedHealthData}
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node Health
|
Current {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node Health
|
||||||
</h4>
|
</h4>
|
||||||
<Pie
|
<Pie
|
||||||
{useAltColors}
|
{useAltColors}
|
||||||
|
|||||||
Reference in New Issue
Block a user