Merge branch 'dev' of github.com:ClusterCockpit/cc-backend into dev

This commit is contained in:
2026-01-22 20:31:18 +01:00
10 changed files with 445 additions and 776 deletions

View File

@@ -305,8 +305,13 @@ func (auth *Authentication) SaveSession(rw http.ResponseWriter, r *http.Request,
if auth.SessionMaxAge != 0 { if auth.SessionMaxAge != 0 {
session.Options.MaxAge = int(auth.SessionMaxAge.Seconds()) session.Options.MaxAge = int(auth.SessionMaxAge.Seconds())
} }
if config.Keys.HTTPSCertFile == "" && config.Keys.HTTPSKeyFile == "" { if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
cclog.Warn("HTTPS not configured - session cookies will not have Secure flag set (insecure for production)") // If neither TLS or an encrypted reverse proxy are used, do not mark cookies as secure.
cclog.Warn("Authenticating with unencrypted request. Session cookies will not have Secure flag set (insecure for production)")
if r.Header.Get("X-Forwarded-Proto") == "" {
// This warning will not be printed if e.g. X-Forwarded-Proto == http
cclog.Warn("If you are using a reverse proxy, make sure X-Forwarded-Proto is set")
}
session.Options.Secure = false session.Options.Secure = false
} }
session.Options.SameSite = http.SameSiteStrictMode session.Options.SameSite = http.SameSiteStrictMode

View File

@@ -6,9 +6,6 @@
--> -->
<script> <script>
// import {
// getContext
// } from "svelte"
import { import {
queryStore, queryStore,
gql, gql,
@@ -55,9 +52,6 @@
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
let colWidthStates = $state(0); let colWidthStates = $state(0);
let colWidthRoof = $state(0);
let colWidthTotals = $state(0);
let colWidthStacked = $state(0);
/* Derived */ /* Derived */
// States for Stacked charts // States for Stacked charts
@@ -354,9 +348,7 @@
</script> </script>
<Card style="height: 98vh;"> <Row>
<CardBody class="align-content-center p-2">
<Row>
<Col> <Col>
<Refresher <Refresher
hideSelector hideSelector
@@ -371,15 +363,16 @@
}} }}
/> />
</Col> </Col>
</Row> </Row>
{#if $statusQuery.fetching || $statesTimed.fetching}
{#if $statusQuery.fetching || $statesTimed.fetching}
<Row class="justify-content-center"> <Row class="justify-content-center">
<Col xs="auto"> <Col xs="auto">
<Spinner /> <Spinner />
</Col> </Col>
</Row> </Row>
{:else if $statusQuery.error || $statesTimed.error} {:else if $statusQuery.error || $statesTimed.error}
<Row class="mb-2"> <Row class="mb-2">
<Col class="d-flex justify-content-end"> <Col class="d-flex justify-content-end">
<Button color="secondary" href="/"> <Button color="secondary" href="/">
@@ -400,8 +393,10 @@
{/if} {/if}
</Row> </Row>
{:else} {:else}
<Row cols={{xs:1, md:2}}> <!-- View Supposed to be Viewed at Max Viewport Size -->
<div class="align-content-center p-2">
<Row cols={{xs:1, md:2}} style="height: 24vh; margin-bottom: 1rem;">
<Col> <!-- General Cluster Info Card --> <Col> <!-- General Cluster Info Card -->
<Card class="h-100"> <Card class="h-100">
<CardHeader> <CardHeader>
@@ -521,40 +516,55 @@
</CardBody> </CardBody>
</Card> </Card>
</Col> </Col>
</Row>
<Col> <!-- Total Cluster Metric in Time SUMS--> <Row cols={{xs:1, md:2}} style="height: 35vh; margin-bottom: 1rem;">
<div bind:clientWidth={colWidthTotals}> <!-- Total Cluster Metric in Time SUMS-->
<Col class="text-center">
<h5 class="mt-2 mb-0">
Cluster Utilization (
<span style="color: #0000ff;">
{`${$statusQuery?.data?.clusterMetrics?.metrics[0]?.name} (${$statusQuery?.data?.clusterMetrics?.metrics[0]?.unit?.prefix}${$statusQuery?.data?.clusterMetrics?.metrics[0]?.unit?.base})`}
</span>,
<span style="color: #ff0000;">
{`${$statusQuery?.data?.clusterMetrics?.metrics[1]?.name} (${$statusQuery?.data?.clusterMetrics?.metrics[1]?.unit?.prefix}${$statusQuery?.data?.clusterMetrics?.metrics[1]?.unit?.base})`}
</span>
)
</h5>
<div>
{#key $statusQuery?.data?.clusterMetrics}
<DoubleMetric <DoubleMetric
width={colWidthTotals}
timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60} timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60}
numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0} numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0}
metricData={$statusQuery?.data?.clusterMetrics?.metrics || []} metricData={$statusQuery?.data?.clusterMetrics?.metrics || []}
cluster={presetCluster} publicMode
fixLinewidth={2}
/> />
{/key}
</div> </div>
</Col> </Col>
<Col> <!-- Nodes Roofline --> <Col> <!-- Nodes Roofline -->
<div bind:clientWidth={colWidthRoof}> <div>
{#key $statusQuery?.data?.nodeMetrics} {#key $statusQuery?.data?.nodeMetrics}
<Roofline <Roofline
colorBackground colorBackground
useColors={false} useColors={false}
useLegend={false} useLegend={false}
allowSizeChange allowSizeChange
width={colWidthRoof}
height={300}
cluster={presetCluster} cluster={presetCluster}
subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null} subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null}
roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics)} roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics)}
nodesData={transformNodesStatsToInfo($statusQuery?.data?.nodeMetrics)} nodesData={transformNodesStatsToInfo($statusQuery?.data?.nodeMetrics)}
fixTitle="Node Utilization" fixTitle="Node Utilization"
yMinimum={1.0} yMinimum={1.0}
height={330}
/> />
{/key} {/key}
</div> </div>
</Col> </Col>
</Row>
<Row cols={{xs:1, md:2}} style="height: 35vh;">
<Col> <!-- Pie Last States --> <Col> <!-- Pie Last States -->
<Row> <Row>
{#if refinedStateData.length > 0} {#if refinedStateData.length > 0}
@@ -607,12 +617,11 @@
</Col> </Col>
<Col> <!-- Stacked SchedState --> <Col> <!-- Stacked SchedState -->
<div bind:clientWidth={colWidthStacked}> <div>
{#key $statesTimed?.data?.nodeStatesTimed} {#key $statesTimed?.data?.nodeStatesTimed}
<Stacked <Stacked
data={$statesTimed?.data?.nodeStatesTimed} data={$statesTimed?.data?.nodeStatesTimed}
width={colWidthStacked} height={300}
height={260}
ylabel="Nodes" ylabel="Nodes"
yunit = "#Count" yunit = "#Count"
title = "Cluster Status" title = "Cluster Status"
@@ -622,6 +631,5 @@
</div> </div>
</Col> </Col>
</Row> </Row>
{/if} </div>
</CardBody> {/if}
</Card>

View File

@@ -229,7 +229,7 @@
</DropdownToggle> </DropdownToggle>
<DropdownMenu class="dropdown-menu-lg-end"> <DropdownMenu class="dropdown-menu-lg-end">
<NavbarLinks <NavbarLinks
{clustersNames} {clusterNames}
{subclusterMap} {subclusterMap}
direction="right" direction="right"
links={views.filter( links={views.filter(

View File

@@ -4,123 +4,56 @@
Only width/height should change reactively. Only width/height should change reactively.
Properties: Properties:
- `metric String`: The metric name - `metricData [Data]`: Two series of metric data including unit info
- `scope String?`: Scope of the displayed data [Default: node] - `timestep Number`: Data timestep
- `numNodes Number`: Number of nodes from which metric data is aggregated
- `cluster String`: Cluster name of the parent job / data [Default: ""]
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: true]
- `enableFlip Bool?`: Whether to use legend tooltip flipping based on canvas size [Default: false]
- `publicMode Bool?`: Disables tooltip legend and enables larger colored axis labels [Default: false]
- `height Number?`: The plot height [Default: 300] - `height Number?`: The plot height [Default: 300]
- `timestep Number`: The timestep used for X-axis rendering
- `series [GraphQL.Series]`: The metric data object
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
- `cluster String?`: Cluster name of the parent job / data [Default: ""]
- `subCluster String`: Name of the subCluster of the parent job
- `isShared Bool?`: If this job used shared resources; for additional legend display [Default: false]
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false]
- `numhwthreads Number?`: Number of job HWThreads [Default: 0]
- `numaccs Number?`: Number of job Accelerators [Default: 0]
- `zoomState Object?`: The last zoom state to preserve on user zoom [Default: null]
- `thersholdState Object?`: The last threshold state to preserve on user zoom [Default: null]
- `extendedLegendData Object?`: Additional information to be rendered in an extended legend [Default: null]
- `onZoom Func`: Callback function to handle zoom-in event
--> -->
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
import { formatNumber, formatDurationTime } from "../units.js"; import { formatNumber, formatDurationTime } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
// metric, metricData,
width = 0,
height = 300,
fixLinewidth = null,
timestep, timestep,
numNodes, numNodes,
metricData, cluster,
// useStatsSeries = false,
// statisticsSeries = null,
cluster = "",
forNode = true, forNode = true,
// zoomState = null,
// thresholdState = null,
enableFlip = false, enableFlip = false,
// onZoom publicMode = false,
height = 300,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
const clusterCockpitConfig = getContext("cc-config"); const clusterCockpitConfig = getContext("cc-config");
// const resampleConfig = getContext("resampling"); const fixedLineColors = ["#0000ff", "#ff0000"]; // Plot only uses 2 Datasets: High Contrast
// const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
// const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
const lineColors = clusterCockpitConfig.plotConfiguration_colorScheme;
// const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
const renderSleepTime = 200; const renderSleepTime = 200;
// const normalLineColor = "#000000";
// const backgroundColors = {
// normal: "rgba(255, 255, 255, 1.0)",
// caution: cbmode ? "rgba(239, 230, 69, 0.3)" : "rgba(255, 128, 0, 0.3)",
// alert: cbmode ? "rgba(225, 86, 44, 0.3)" : "rgba(255, 0, 0, 0.3)",
// };
/* Var Init */ /* Var Init */
let timeoutId = null; let timeoutId = null;
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null); let uplot = $state(null);
/* Derived */ /* Derived */
const lineWidth = $derived(fixLinewidth ? fixLinewidth : clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio);
// const usesMeanStatsSeries = $derived((statisticsSeries?.mean && statisticsSeries.mean.length != 0));
// const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null);
// const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null);
// const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null);
// const thresholds = $derived(findJobAggregationThresholds(
// subClusterTopology,
// metricConfig,
// scope,
// numhwthreads,
// numaccs
// ));
const longestSeries = $derived.by(() => {
// if (useStatsSeries) {
// return usesMeanStatsSeries ? statisticsSeries?.mean?.length : statisticsSeries?.median?.length;
// } else {
return metricData.reduce((n, m) => Math.max(n, m.data.length), 0);
// }
});
const maxX = $derived(longestSeries * timestep); const maxX = $derived(longestSeries * timestep);
// const maxY = $derived.by(() => { const lineWidth = $derived(publicMode ? 2 : clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio);
// let pendingY = 0; const longestSeries = $derived.by(() => {
// // if (useStatsSeries) { return metricData.reduce((n, m) => Math.max(n, m.data.length), 0);
// // pendingY = statisticsSeries.max.reduce( });
// // (max, x) => Math.max(max, x),
// // thresholds?.normal,
// // ) || thresholds?.normal
// // } else {
// pendingY = series.reduce(
// (max, series) => Math.max(max, series?.statistics?.max),
// thresholds?.normal,
// ) || thresholds?.normal;
// // }
// if (pendingY >= 10 * thresholds.peak) { // Derive Plot Params
// // Hard y-range render limit if outliers in series data let plotData = $derived.by(() => {
// return (10 * thresholds.peak);
// } else {
// return pendingY;
// }
// });
// const plotBands = $derived.by(() => {
// if (useStatsSeries) {
// return [
// { series: [2, 3], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
// { series: [3, 1], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
// ];
// };
// return null;
// })
const plotData = $derived.by(() => {
let pendingData = [new Array(longestSeries)]; let pendingData = [new Array(longestSeries)];
// X // X
if (forNode === true) { if (forNode === true) {
@@ -135,25 +68,15 @@
}; };
}; };
// Y // Y
// if (useStatsSeries) {
// pendingData.push(statisticsSeries.min);
// pendingData.push(statisticsSeries.max);
// if (usesMeanStatsSeries) {
// pendingData.push(statisticsSeries.mean);
// } else {
// pendingData.push(statisticsSeries.median);
// }
// } else {
for (let i = 0; i < metricData.length; i++) { for (let i = 0; i < metricData.length; i++) {
pendingData.push(metricData[i]?.data); pendingData.push(metricData[i]?.data);
}; };
// };
return pendingData; return pendingData;
}) })
const plotSeries = $derived.by(() => {
let plotSeries = $derived.by(() => {
// X
let pendingSeries = [ let pendingSeries = [
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
{ {
label: "Runtime", label: "Runtime",
value: (u, ts, sidx, didx) => value: (u, ts, sidx, didx) =>
@@ -161,87 +84,108 @@
} }
]; ];
// Y // Y
// if (useStatsSeries) {
// pendingSeries.push({
// label: "min",
// scale: "y",
// width: lineWidth,
// stroke: cbmode ? "rgb(0,255,0)" : "red",
// });
// pendingSeries.push({
// label: "max",
// scale: "y",
// width: lineWidth,
// stroke: cbmode ? "rgb(0,0,255)" : "green",
// });
// pendingSeries.push({
// label: usesMeanStatsSeries ? "mean" : "median",
// scale: "y",
// width: lineWidth,
// stroke: "black",
// });
// } else {
for (let i = 0; i < metricData.length; i++) { for (let i = 0; i < metricData.length; i++) {
// Default
// if (!extendedLegendData) {
pendingSeries.push({ pendingSeries.push({
label: `${metricData[i]?.name} (${metricData[i]?.unit?.prefix}${metricData[i]?.unit?.base})`, label: publicMode ? null : `${metricData[i]?.name} (${metricData[i]?.unit?.prefix}${metricData[i]?.unit?.base})`,
scale: `y${i+1}`, scale: `y${i+1}`,
width: lineWidth, width: lineWidth,
stroke: lineColor(i, metricData.length), stroke: fixedLineColors[i],
}); });
// }
// Extended Legend For NodeList
// else {
// pendingSeries.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: formatDurationTime(plotData[0][idx], forNode),
// value: plotData[sidx][idx],
// user: extendedLegendData[series[i].id].user,
// job: extendedLegendData[series[i].id].job,
// };
// } else {
// return {
// time: formatDurationTime(plotData[0][idx], forNode),
// value: plotData[sidx][idx],
// user: '-',
// job: '-',
// };
// }
// }
// });
// }
// };
}; };
return pendingSeries; return pendingSeries;
}) })
/* Effects */ // Set Options
// $effect(() => { function getOpts(optWidth, optHeight) {
// if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true; let baseOpts = {
// }) width: optWidth,
height: optHeight,
series: plotSeries,
axes: [
{
scale: "x",
incrs: timeIncrs(timestep, maxX, forNode),
values: (_, vals) => vals.map((v) => formatDurationTime(v, forNode)),
},
{
scale: "y1",
grid: { show: true },
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
side: 1,
scale: "y2",
grid: { show: false },
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
// bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {},
scales: {
x: { time: false },
y1: { auto: true },
y2: { auto: true },
},
legend: {
show: !publicMode,
live: !publicMode
},
cursor: {
drag: { x: true, y: true },
}
}
if (publicMode) {
// X
baseOpts.axes[0].space = 60;
baseOpts.axes[0].font = '16px Arial';
// Y1
baseOpts.axes[1].space = 50;
baseOpts.axes[1].size = 60;
baseOpts.axes[1].font = '16px Arial';
baseOpts.axes[1].stroke = fixedLineColors[0];
// Y2
baseOpts.axes[2].space = 40;
baseOpts.axes[2].size = 60;
baseOpts.axes[2].font = '16px Arial';
baseOpts.axes[2].stroke = fixedLineColors[1];
} else {
baseOpts.title = 'Cluster Utilization';
baseOpts.plugins = [legendAsTooltipPlugin()];
// X
baseOpts.axes[0].label = 'Time';
// Y1
baseOpts.axes[1].label = `${metricData[0]?.name} (${metricData[0]?.unit?.prefix}${metricData[0]?.unit?.base})`;
// Y2
baseOpts.axes[2].label = `${metricData[1]?.name} (${metricData[1]?.unit?.prefix}${metricData[1]?.unit?.base})`;
baseOpts.hooks.draw = [
(u) => {
// Draw plot type label:
let textl = `Cluster ${cluster}`
let textr = `Sums of ${numNodes} nodes`
u.ctx.save();
u.ctx.textAlign = "start"; // 'end'
u.ctx.fillStyle = "black";
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
u.ctx.textAlign = "end";
u.ctx.fillStyle = "black";
u.ctx.fillText(
textr,
u.bbox.left + u.bbox.width - 10,
u.bbox.top + (forNode ? 0 : 10),
);
u.ctx.restore();
return;
},
]
}
return baseOpts;
};
/* Effects */
// This updates plot on all size changes if wrapper (== data) exists // This updates plot on all size changes if wrapper (== data) exists
$effect(() => { $effect(() => {
if (plotWrapper) { if (plotWrapper) {
@@ -262,73 +206,6 @@
} }
} }
// removed arg "subcluster": input metricconfig and topology now directly derived from subcluster
// function findJobAggregationThresholds(
// subClusterTopology,
// metricConfig,
// scope,
// numhwthreads,
// numaccs
// ) {
// if (!subClusterTopology || !metricConfig || !scope) {
// console.warn("Argument missing for findJobAggregationThresholds!");
// return null;
// }
// // handle special *-stat scopes
// if (scope.match(/(.*)-stat$/)) {
// const statParts = scope.split('-');
// scope = statParts[0]
// }
// if (metricConfig?.aggregation == "avg") {
// // Return as Configured
// return {
// normal: metricConfig.normal,
// caution: metricConfig.caution,
// alert: metricConfig.alert,
// peak: metricConfig.peak,
// };
// }
// if (metricConfig?.aggregation == "sum") {
// // Scale Thresholds
// let fraction;
// if (numaccs > 0) fraction = subClusterTopology.accelerators.length / numaccs;
// else if (numhwthreads > 0) fraction = subClusterTopology.core.length / numhwthreads;
// else fraction = 1; // Fallback
// let divisor;
// // Exclusive: Fraction = 1; Shared: Fraction > 1
// if (scope == 'node') divisor = fraction;
// // Cap divisor at number of available sockets or domains
// else if (scope == 'socket') divisor = (fraction < subClusterTopology.socket.length) ? subClusterTopology.socket.length : fraction;
// else if (scope == "memoryDomain") divisor = (fraction < subClusterTopology.memoryDomain.length) ? subClusterTopology.socket.length : fraction;
// // Use Maximum Division for Smallest Scopes
// else if (scope == "core") divisor = subClusterTopology.core.length;
// else if (scope == "hwthread") divisor = subClusterTopology.core.length; // alt. name for core
// else if (scope == "accelerator") divisor = subClusterTopology.accelerators.length;
// else {
// console.log('Unknown scope, return default aggregation thresholds for sum', scope)
// divisor = 1;
// }
// return {
// peak: metricConfig.peak / divisor,
// normal: metricConfig.normal / divisor,
// caution: metricConfig.caution / divisor,
// alert: metricConfig.alert / divisor,
// };
// }
// console.warn(
// "Missing or unkown aggregation mode (sum/avg) for metric:",
// metricConfig,
// );
// return null;
// }
// UPLOT PLUGIN // converts the legend into a simple tooltip // UPLOT PLUGIN // converts the legend into a simple tooltip
function legendAsTooltipPlugin({ function legendAsTooltipPlugin({
className, className,
@@ -408,219 +285,22 @@
} }
} }
// RETURN BG COLOR FROM THRESHOLD function onSizeChange(chgWidth, chgHeight) {
// function backgroundColor() {
// if (
// clusterCockpitConfig.plotConfiguration_colorBackground == false ||
// // !thresholds ||
// !(series && series.every((s) => s.statistics != null))
// )
// return backgroundColors.normal;
// let cond =
// thresholds.alert < thresholds.caution
// ? (a, b) => a <= b
// : (a, b) => a >= b;
// let avg =
// series.reduce((sum, series) => sum + series.statistics.avg, 0) /
// series.length;
// if (Number.isNaN(avg)) return backgroundColors.normal;
// if (cond(avg, thresholds.alert)) return backgroundColors.alert;
// if (cond(avg, thresholds.caution)) return backgroundColors.caution;
// return backgroundColors.normal;
// }
function lineColor(i, n) {
if (n && n >= lineColors.length) return lineColors[i % lineColors.length];
else return lineColors[Math.floor((i / n) * lineColors.length)];
}
function render(ren_width, ren_height) {
// Set Options
const opts = {
width,
height,
title: 'Cluster Utilization',
plugins: [legendAsTooltipPlugin()],
series: plotSeries,
axes: [
{
scale: "x",
space: 35,
incrs: timeIncrs(timestep, maxX, forNode),
label: "Time",
values: (_, vals) => vals.map((v) => formatDurationTime(v, forNode)),
},
{
scale: "y1",
grid: { show: true },
label: `${metricData[0]?.name} (${metricData[0]?.unit?.prefix}${metricData[0]?.unit?.base})`,
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
{
side: 1,
scale: "y2",
grid: { show: false },
label: `${metricData[1]?.name} (${metricData[1]?.unit?.prefix}${metricData[1]?.unit?.base})`,
values: (u, vals) => vals.map((v) => formatNumber(v)),
},
],
// bands: plotBands,
padding: [5, 10, -20, 0],
hooks: {
// init: [
// (u) => {
// /* IF Zoom Enabled */
// if (resampleConfig && !forNode) {
// u.over.addEventListener("dblclick", (e) => {
// // console.log('Dispatch: Zoom Reset')
// onZoom({
// lastZoomState: {
// x: { time: false },
// y: { auto: true }
// }
// });
// });
// };
// },
// ],
draw: [
(u) => {
// Draw plot type label:
let textl = `Cluster ${cluster}`
// let textl = `${scope}${plotSeries.length > 2 ? "s" : ""}${
// useStatsSeries
// ? (usesMeanStatsSeries ? ": min/mean/max" : ": min/median/max")
// : metricConfig != null && scope != metricConfig.scope
// ? ` (${metricConfig.aggregation})`
// : ""
// }`;
let textr = `Sums of ${numNodes} nodes`
//let textr = `${isShared && scope != "core" && scope != "accelerator" ? "[Shared]" : ""}`;
u.ctx.save();
u.ctx.textAlign = "start"; // 'end'
u.ctx.fillStyle = "black";
u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10));
u.ctx.textAlign = "end";
u.ctx.fillStyle = "black";
u.ctx.fillText(
textr,
u.bbox.left + u.bbox.width - 10,
u.bbox.top + (forNode ? 0 : 10),
);
// u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right
// if (!thresholds) {
u.ctx.restore();
return;
// }
// let y = u.valToPos(thresholds.normal, "y", true);
// u.ctx.save();
// u.ctx.lineWidth = lineWidth;
// u.ctx.strokeStyle = normalLineColor;
// u.ctx.setLineDash([5, 5]);
// u.ctx.beginPath();
// u.ctx.moveTo(u.bbox.left, y);
// u.ctx.lineTo(u.bbox.left + u.bbox.width, y);
// u.ctx.stroke();
// u.ctx.restore();
},
],
// setScale: [
// (u, key) => { // If ZoomResample is Configured && Not System/Node View
// if (resampleConfig && !forNode && key === 'x') {
// const numX = (u.series[0].idxs[1] - u.series[0].idxs[0])
// if (numX <= resampleTrigger && timestep !== resampleMinimum) {
// /* Get closest zoom level; prevents multiple iterative zoom requests for big zoom-steps (e.g. 600 -> 300 -> 120 -> 60) */
// // Which resolution to theoretically request to achieve 30 or more visible data points:
// const target = (numX * timestep) / resampleTrigger
// // Which configured resolution actually matches the closest to theoretical target:
// const closest = resampleResolutions.reduce(function(prev, curr) {
// return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev);
// });
// // Prevents non-required dispatches
// if (timestep !== closest) {
// // console.log('Dispatch: Zoom with Res from / to', timestep, closest)
// onZoom({
// newRes: closest,
// lastZoomState: u?.scales,
// lastThreshold: thresholds?.normal
// });
// }
// } else {
// // console.log('Dispatch: Zoom Update States')
// onZoom({
// lastZoomState: u?.scales,
// lastThreshold: thresholds?.normal
// });
// };
// };
// },
// ]
},
scales: {
x: { time: false },
y1: { auto: true },
y1: { auto: true },
},
legend: {
// Display legend until max 12 Y-dataseries
show: true, // metricData.length <= 12 || useStatsSeries,
live: true // But This Plot always for 2 Data-Series
},
cursor: {
drag: { x: true, y: true },
}
};
// Handle Render
if (!uplot) {
opts.width = ren_width;
opts.height = ren_height;
// if (plotSync) {
// opts.cursor.sync = {
// key: plotSync.key,
// scales: ["x", null],
// }
// }
// if (zoomState && metricConfig?.aggregation == "avg") {
// opts.scales = {...zoomState}
// } else if (zoomState && metricConfig?.aggregation == "sum") {
// // Allow Zoom In === Ymin changed
// if (zoomState.y.min !== 0) { // scope change?: only use zoomState if thresholds match
// if ((thresholdState === thresholds?.normal)) { opts.scales = {...zoomState} };
// } // else: reset scaling to default
// }
uplot = new uPlot(opts, plotData, plotWrapper);
} else {
uplot.setSize({ width: ren_width, height: ren_height });
}
}
function onSizeChange(chg_width, chg_height) {
if (!uplot) return;
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null; timeoutId = null;
render(chg_width, chg_height); render(chgWidth, chgHeight);
}, renderSleepTime); }, renderSleepTime);
} }
/* On Mount */ function render(renWidth, renHeight) {
onMount(() => { if (!uplot) {
if (plotWrapper) { let opts = getOpts(renWidth, renHeight);
render(width, height); uplot = new uPlot(opts, plotData, plotWrapper);
} else {
uplot.setSize({ width: renWidth, height: renHeight });
}
} }
});
/* On Destroy */ /* On Destroy */
onDestroy(() => { onDestroy(() => {
@@ -635,8 +315,12 @@
<div bind:this={plotWrapper} bind:clientWidth={width} <div bind:this={plotWrapper} bind:clientWidth={width}
class={forNode ? 'py-2 rounded' : 'rounded'} class={forNode ? 'py-2 rounded' : 'rounded'}
></div> ></div>
{:else if cluster}
<Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{cluster}</code>.</Card
>
{:else} {:else}
<Card body color="warning" class="mx-4" <Card body color="warning" class="mx-4"
>Cannot render plot: No series data returned for <code>{cluster}</code></Card >Cannot render plot: No series data returned.</Card
> >
{/if} {/if}

View File

@@ -56,7 +56,6 @@
/* Const Init */ /* Const Init */
const clusterCockpitConfig = getContext("cc-config"); const clusterCockpitConfig = getContext("cc-config");
const resampleConfig = getContext("resampling"); const resampleConfig = getContext("resampling");
const lineColors = clusterCockpitConfig.plotConfiguration_colorScheme;
const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio; const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio;
const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false; const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
const renderSleepTime = 200; const renderSleepTime = 200;
@@ -200,7 +199,7 @@
: scope + " #" + (i + 1), : scope + " #" + (i + 1),
scale: "y", scale: "y",
width: lineWidth, width: lineWidth,
stroke: lineColor(i, series?.length), stroke: lineColor(i, clusterCockpitConfig.plotConfiguration_colorScheme),
}); });
} }
// Extended Legend For NodeList // Extended Legend For NodeList
@@ -214,7 +213,7 @@
: scope + " #" + (i + 1), : scope + " #" + (i + 1),
scale: "y", scale: "y",
width: lineWidth, width: lineWidth,
stroke: lineColor(i, series?.length), stroke: lineColor(i, clusterCockpitConfig.plotConfiguration_colorScheme),
values: (u, sidx, idx) => { values: (u, sidx, idx) => {
// "i" = "sidx - 1" : sidx contains x-axis-data // "i" = "sidx - 1" : sidx contains x-axis-data
if (idx == null) if (idx == null)
@@ -446,9 +445,8 @@
return backgroundColors.normal; return backgroundColors.normal;
} }
function lineColor(i, n) { function lineColor(index, colors) {
if (n && n >= lineColors.length) return lineColors[i % lineColors.length]; return colors[index % colors.length];
else return lineColors[Math.floor((i / n) * lineColors.length)];
} }
function render(ren_width, ren_height) { function render(ren_width, ren_height) {

View File

@@ -40,8 +40,7 @@
useColors = true, useColors = true,
useLegend = true, useLegend = true,
colorBackground = false, colorBackground = false,
width = 600, height = 300,
height = 380,
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -53,11 +52,12 @@
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null); let uplot = $state(null);
/* Effect */ /* Effect */
$effect(() => { $effect(() => {
if (allowSizeChange) sizeChanged(width, height); if (allowSizeChange) onSizeChange(width, height);
}); });
// Copied Example Vars for Uplot Bubble // Copied Example Vars for Uplot Bubble
@@ -517,11 +517,11 @@
} }
// Main Functions // Main Functions
function sizeChanged() { function onSizeChange() {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
timeoutId = null; timeoutId = null;
if (uplot) uplot.destroy(); if (uplot) uplot.destroy(); // Prevents Multi-Render
render(roofData, jobsData, nodesData); render(roofData, jobsData, nodesData);
}, 200); }, 200);
} }
@@ -995,7 +995,7 @@
</script> </script>
{#if roofData != null} {#if roofData != null}
<div bind:this={plotWrapper} class="p-2"></div> <div bind:this={plotWrapper} bind:clientWidth={width} class="p-2"></div>
{:else} {:else}
<Card class="mx-4 my-2" body color="warning">Cannot render roofline: No data!</Card> <Card class="mx-4 my-2" body color="warning">Cannot render roofline: No data!</Card>
{/if} {/if}

View File

@@ -2,7 +2,6 @@
@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
Properties: Properties:
- `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]
- `xlabel String?`: Plot X axis label [Default: ""] - `xlabel String?`: Plot X axis label [Default: ""]
@@ -15,12 +14,11 @@
<script> <script>
import uPlot from "uplot"; import uPlot from "uplot";
import { formatUnixTime } from "../units.js"; import { formatUnixTime } from "../units.js";
import { getContext, onMount, onDestroy } from "svelte"; import { getContext, onDestroy } from "svelte";
import { Card } from "@sveltestrap/sveltestrap"; import { Card } from "@sveltestrap/sveltestrap";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
width = 0,
height = 300, height = 300,
data = null, data = null,
xlabel = null, xlabel = null,
@@ -129,17 +127,17 @@
}; };
} }
function getStackedOpts(title, width, height, series, data) { function getStackedOpts(optTitle, optWidth, optHeight, optSeries, optData) {
let opts = { let opts = {
width, width: optWidth,
height, height: optHeight,
title, title: optTitle,
plugins: [legendAsTooltipPlugin()], plugins: [legendAsTooltipPlugin()],
series, series: optSeries,
axes: [ axes: [
{ {
scale: "x", scale: "x",
space: 25, // Tick Spacing // space: 25, // Tick Spacing
rotate: 30, rotate: 30,
show: true, show: true,
label: xlabel, label: xlabel,
@@ -168,25 +166,25 @@
} }
}; };
let stacked = stack(data, i => false); let stacked = stack(optData, i => false);
opts.bands = stacked.bands; opts.bands = stacked.bands;
opts.cursor = opts.cursor || {}; opts.cursor = opts.cursor || {};
opts.cursor.dataIdx = (u, seriesIdx, closestIdx, xValue) => { opts.cursor.dataIdx = (u, seriesIdx, closestIdx, xValue) => {
return data[seriesIdx][closestIdx] == null ? null : closestIdx; return optData[seriesIdx][closestIdx] == null ? null : closestIdx;
}; };
opts.series.forEach(s => { opts.series.forEach(s => {
// Format Time Info from Unix TS to LocalTimeString // Format Time Info from Unix TS to LocalTimeString
s.value = (u, v, si, i) => (si === 0) ? formatUnixTime(data[si][i]) : data[si][i]; s.value = (u, v, si, i) => (si === 0) ? formatUnixTime(optData[si][i]) : optData[si][i];
s.points = s.points || {}; s.points = s.points || {};
// scan raw unstacked data to return only real points // scan raw unstacked optData to return only real points
s.points.filter = (u, seriesIdx, show, gaps) => { s.points.filter = (u, seriesIdx, show, gaps) => {
if (show) { if (show) {
let pts = []; let pts = [];
data[seriesIdx].forEach((v, i) => { optData[seriesIdx].forEach((v, i) => {
v != null && pts.push(i); v != null && pts.push(i);
}); });
return pts; return pts;
@@ -206,7 +204,7 @@
opts.hooks = { opts.hooks = {
setSeries: [ setSeries: [
(u, i) => { (u, i) => {
let stacked = stack(data, i => !u.series[i].show); let stacked = stack(optData, i => !u.series[i].show);
u.delBand(null); u.delBand(null);
stacked.bands.forEach(b => u.addBand(b)); stacked.bands.forEach(b => u.addBand(b));
u.setData(stacked.data); u.setData(stacked.data);
@@ -298,10 +296,11 @@
/* Var Init */ /* Var Init */
let timeoutId = null; let timeoutId = null;
let uplot = null;
/* State Init */ /* State Init */
let plotWrapper = $state(null); let plotWrapper = $state(null);
let width = $state(0); // Wrapper Width
let uplot = $state(null);
/* Effects */ /* Effects */
$effect(() => { $effect(() => {
@@ -311,6 +310,14 @@
}); });
/* Functions */ /* Functions */
function onSizeChange(chg_width, chg_height) {
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
render(chg_width, chg_height);
}, 200);
}
function render(ren_width, ren_height) { function render(ren_width, ren_height) {
if (!uplot) { if (!uplot) {
let { opts, data } = getStackedOpts(title, ren_width, ren_height, plotSeries, collectData); let { opts, data } = getStackedOpts(title, ren_width, ren_height, plotSeries, collectData);
@@ -320,22 +327,6 @@
} }
} }
function onSizeChange(chg_width, chg_height) {
if (!uplot) return;
if (timeoutId != null) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
render(chg_width, chg_height);
}, 200);
}
/* On Mount */
onMount(() => {
if (plotWrapper) {
render(width, height);
}
});
/* On Destroy */ /* On Destroy */
onDestroy(() => { onDestroy(() => {
if (timeoutId != null) clearTimeout(timeoutId); if (timeoutId != null) clearTimeout(timeoutId);

View File

@@ -57,7 +57,7 @@ export function formatUnixTime(t, withDate = false) {
return t; return t;
} else { } else {
if (withDate) return new Date(t * 1000).toLocaleString(); if (withDate) return new Date(t * 1000).toLocaleString();
else return new Date(t * 1000).toLocaleTimeString(); else return new Date(t * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
} }
} }
} }

View File

@@ -58,10 +58,6 @@
let to = $state(new Date(Date.now())); let to = $state(new Date(Date.now()));
let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
let colWidthJobs = $state(0); let colWidthJobs = $state(0);
let colWidthRoof = $state(0);
let colWidthTotals =$state(0);
let colWidthStacked1 = $state(0);
let colWidthStacked2 = $state(0);
/* Derived */ /* Derived */
// States for Stacked charts // States for Stacked charts
@@ -531,13 +527,11 @@
</Col> </Col>
<Col> <!-- Job Roofline --> <Col> <!-- Job Roofline -->
<div bind:clientWidth={colWidthRoof}> <div>
{#key $statusQuery?.data?.jobsMetricStats} {#key $statusQuery?.data?.jobsMetricStats}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={colWidthRoof}
height={300}
subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null} subCluster={clusterInfo?.roofData ? clusterInfo.roofData : null}
roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats)} roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats)}
jobsData={transformJobsStatsToInfo($statusQuery?.data?.jobsMetricStats)} jobsData={transformJobsStatsToInfo($statusQuery?.data?.jobsMetricStats)}
@@ -547,24 +541,23 @@
</Col> </Col>
<Col> <!-- Total Cluster Metric in Time SUMS--> <Col> <!-- Total Cluster Metric in Time SUMS-->
<div bind:clientWidth={colWidthTotals}> <div>
{#key $statusQuery?.data?.clusterMetrics}
<DoubleMetric <DoubleMetric
width={colWidthTotals}
timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60} timestep={$statusQuery?.data?.clusterMetrics[0]?.timestep || 60}
numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0} numNodes={$statusQuery?.data?.clusterMetrics?.nodeCount || 0}
metricData={$statusQuery?.data?.clusterMetrics?.metrics || []} metricData={$statusQuery?.data?.clusterMetrics?.metrics || []}
cluster={presetCluster} cluster={presetCluster}
fixLinewidth={2}
/> />
{/key}
</div> </div>
</Col> </Col>
<Col> <!-- Stacked SchedState --> <Col> <!-- Stacked SchedState -->
<div bind:clientWidth={colWidthStacked1}> <div>
{#key $statesTimed?.data?.nodeStates} {#key $statesTimed?.data?.nodeStates}
<Stacked <Stacked
data={$statesTimed?.data?.nodeStates} data={$statesTimed?.data?.nodeStates}
width={colWidthStacked1}
height={330} height={330}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
@@ -577,11 +570,10 @@
</Col> </Col>
<Col> <!-- Stacked Healthstate --> <Col> <!-- Stacked Healthstate -->
<div bind:clientWidth={colWidthStacked2}> <div>
{#key $statesTimed?.data?.healthStates} {#key $statesTimed?.data?.healthStates}
<Stacked <Stacked
data={$statesTimed?.data?.healthStates} data={$statesTimed?.data?.healthStates}
width={colWidthStacked2}
height={330} height={330}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"

View File

@@ -42,9 +42,6 @@
/* State Init */ /* State Init */
let pieWidth = $state(0); let pieWidth = $state(0);
let stackedWidth1 = $state(0);
let stackedWidth2 = $state(0);
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); let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400);
@@ -414,14 +411,13 @@
{#if $statesTimed.data} {#if $statesTimed.data}
<Row cols={{ md: 2 , sm: 1}} class="mb-3 justify-content-center"> <Row cols={{ 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={stackedWidth1}> <div>
{#key $statesTimed?.data?.nodeStates} {#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
data={$statesTimed?.data?.nodeStates} data={$statesTimed?.data?.nodeStates}
width={stackedWidth1 * 0.95}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
yunit = "#Count" yunit = "#Count"
@@ -432,14 +428,13 @@
</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={stackedWidth2}> <div>
{#key $statesTimed?.data?.healthStates} {#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
data={$statesTimed?.data?.healthStates} data={$statesTimed?.data?.healthStates}
width={stackedWidth2 * 0.95}
xlabel="Time" xlabel="Time"
ylabel="Nodes" ylabel="Nodes"
yunit = "#Count" yunit = "#Count"
@@ -628,13 +623,11 @@
</Card> </Card>
</Col> </Col>
<Col class="px-3 mt-2 mt-lg-0"> <Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={plotWidths[i]}> <div>
{#key $statusQuery?.data?.nodeMetrics} {#key $statusQuery?.data?.nodeMetrics}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={plotWidths[i] - 10}
height={300}
cluster={cluster} cluster={cluster}
subCluster={subCluster} subCluster={subCluster}
roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics.filter( roofData={transformNodesStatsToData($statusQuery?.data?.nodeMetrics.filter(
@@ -650,13 +643,11 @@
</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={plotWidths[i]}> <div>
{#key $statusQuery?.data?.jobsMetricStats} {#key $statusQuery?.data?.jobsMetricStats}
<Roofline <Roofline
useColors={true} useColors={true}
allowSizeChange allowSizeChange
width={plotWidths[i] - 10}
height={300}
subCluster={subCluster} subCluster={subCluster}
roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats.filter( roofData={transformJobsStatsToData($statusQuery?.data?.jobsMetricStats.filter(
(data) => data.subCluster == subCluster.name, (data) => data.subCluster == subCluster.name,