move and add interface options for status tabs

This commit is contained in:
Christoph Kluge
2025-08-13 14:22:24 +02:00
parent 44d8254a0b
commit 58ae476a3e
7 changed files with 126 additions and 130 deletions

View File

@@ -2,7 +2,7 @@
@component Main cluster status view component; renders current system-usage information @component Main cluster status view component; renders current system-usage information
Properties: Properties:
- `cluster String`: The cluster to show status information for - `presetCluster String`: The cluster to show status information for
--> -->
<script> <script>
@@ -18,78 +18,45 @@
TabPane TabPane
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import Refresher from "./generic/helper/Refresher.svelte";
import StatusDash from "./status/StatusDash.svelte"; import StatusDash from "./status/StatusDash.svelte";
import UsageDash from "./status/UsageDash.svelte"; import UsageDash from "./status/UsageDash.svelte";
import NodeDash from "./status/NodeDash.svelte";
import StatisticsDash from "./status/StatisticsDash.svelte"; import StatisticsDash from "./status/StatisticsDash.svelte";
import DevelDash from "./status/DevelDash.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
cluster presetCluster
} = $props(); } = $props();
/*Const Init */ /*Const Init */
const useCbColors = getContext("cc-config")?.plot_general_colorblindMode || false const useCbColors = getContext("cc-config")?.plot_general_colorblindMode || false
/* State Init */
let from = $state(new Date(Date.now() - 5 * 60 * 1000));
let to = $state(new Date(Date.now()));
</script> </script>
<!-- Loading indicator & Refresh --> <!-- Loading indicator & Refresh -->
<Row cols={{ xs: 2 }} class="mb-2"> <Row cols={1} class="mb-2">
<Col> <Col>
<h4 class="mb-0">Current utilization of cluster "{cluster}"</h4> <h3 class="mb-0">Current Status of Cluster "{presetCluster.charAt(0).toUpperCase() + presetCluster.slice(1)}"</h3>
</Col>
<Col class="mt-2 mt-md-0">
<Refresher
initially={120}
onRefresh={() => {
from = new Date(Date.now() - 5 * 60 * 1000);
to = new Date(Date.now());
}}
/>
</Col> </Col>
</Row> </Row>
<!-- <Row cols={1} class="text-center mt-3">
<Col>
{#if $initq.fetching || $mainQuery.fetching}
<Spinner />
{:else if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{/if}
</Col>
</Row>
{#if $mainQuery.error}
<Row cols={1}>
<Col>
<Card body color="danger">{$mainQuery.error.message}</Card>
</Col>
</Row>
{/if} -->
<Card class="overflow-auto" style="height: auto;"> <Card class="overflow-auto" style="height: auto;">
<TabContent> <TabContent>
<TabPane tabId="status-dash" tab="Status" active> <TabPane tabId="status-dash" tab="Status" active>
<CardBody> <CardBody>
<StatusDash {cluster} {useCbColors} useAltColors></StatusDash> <StatusDash {presetCluster} {useCbColors} useAltColors></StatusDash>
</CardBody> </CardBody>
</TabPane> </TabPane>
<TabPane tabId="usage-dash" tab="Usage"> <TabPane tabId="usage-dash" tab="Usage">
<CardBody> <CardBody>
<UsageDash {cluster} {useCbColors}></UsageDash> <UsageDash {presetCluster} {useCbColors}></UsageDash>
</CardBody> </CardBody>
</TabPane> </TabPane>
<TabPane tabId="metric-dash" tab="Statistics"> <TabPane tabId="metric-dash" tab="Statistics">
<CardBody> <CardBody>
<StatisticsDash {cluster} {useCbColors}></StatisticsDash> <StatisticsDash {presetCluster} {useCbColors}></StatisticsDash>
</CardBody> </CardBody>
</TabPane> </TabPane>
</TabContent> </TabContent>

View File

@@ -404,6 +404,7 @@
cluster={selectedCluster} cluster={selectedCluster}
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms} presetSelectedHistograms={selectedHistograms}
configName="user_view_histogramMetrics"
applyChange={(newSelection) => { applyChange={(newSelection) => {
selectedHistogramsBuffer[selectedCluster || 'all'] = [...newSelection]; selectedHistogramsBuffer[selectedCluster || 'all'] = [...newSelection];
}} }}

View File

@@ -3,8 +3,9 @@
Properties: Properties:
- `cluster String`: Currently selected cluster - `cluster String`: Currently selected cluster
- `selectedHistograms [String]`: The currently selected metrics to display as histogram
- `ìsOpen Bool`: Is selection opened [Bindable] - `ìsOpen Bool`: Is selection opened [Bindable]
- `configName String`: The config id string to be updated in database on selection change
- `presetSelectedHistograms [String]`: The currently selected metrics to display as histogram
- `applyChange Func`: The callback function to apply current selection - `applyChange Func`: The callback function to apply current selection
--> -->
@@ -25,6 +26,7 @@
let { let {
cluster, cluster,
isOpen = $bindable(), isOpen = $bindable(),
configName,
presetSelectedHistograms, presetSelectedHistograms,
applyChange applyChange
} = $props(); } = $props();
@@ -67,8 +69,8 @@
applyChange(selectedHistograms) applyChange(selectedHistograms)
updateConfiguration({ updateConfiguration({
name: cluster name: cluster
? `user_view_histogramMetrics:${cluster}` ? `${configName}:${cluster}`
: "user_view_histogramMetrics", : configName,
value: selectedHistograms, value: selectedHistograms,
}); });
} }

View File

@@ -5,7 +5,7 @@ import Status from './Status.root.svelte'
mount(Status, { mount(Status, {
target: document.getElementById('svelte-app'), target: document.getElementById('svelte-app'),
props: { props: {
cluster: infos.cluster, presetCluster: infos.cluster,
}, },
context: new Map([ context: new Map([
['cc-config', clusterCockpitConfig] ['cc-config', clusterCockpitConfig]

View File

@@ -2,7 +2,7 @@
@component Main cluster status view component; renders current system-usage information @component Main cluster status view component; renders current system-usage information
Properties: Properties:
- `cluster String`: The cluster to show status information for - `presetCluster String`: The cluster to show status information for
--> -->
<script> <script>
@@ -19,7 +19,6 @@
queryStore, queryStore,
gql, gql,
getContextClient, getContextClient,
// mutationStore,
} from "@urql/svelte"; } from "@urql/svelte";
import { import {
init, init,
@@ -28,10 +27,11 @@
import PlotGrid from "../generic/PlotGrid.svelte"; import PlotGrid from "../generic/PlotGrid.svelte";
import Histogram from "../generic/plots/Histogram.svelte"; import Histogram from "../generic/plots/Histogram.svelte";
import HistogramSelection from "../generic/select/HistogramSelection.svelte"; import HistogramSelection from "../generic/select/HistogramSelection.svelte";
import Refresher from "../generic/helper/Refresher.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
cluster presetCluster
} = $props(); } = $props();
/* Const Init */ /* Const Init */
@@ -40,16 +40,17 @@
const client = getContextClient(); const client = getContextClient();
/* State Init */ /* State Init */
let cluster = $state(presetCluster);
// Histrogram // Histogram
let isHistogramSelectionOpen = $state(false); let isHistogramSelectionOpen = $state(false);
let from = $state(new Date(Date.now() - (30 * 24 * 60 * 60 * 1000))); // Simple way to retrigger GQL: Jobs Started last Month
// TODO: Originally Uses User View Selection! -> Change to Status View let to = $state(new Date(Date.now()));
let selectedHistograms = $state(cluster
? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig['user_view_histogramMetrics'] || []);
/* Derived */ /* Derived */
let selectedHistograms = $derived(cluster
? ccconfig[`status_view_selectedHistograms:${cluster}`] || ( ccconfig['status_view_selectedHistograms'] || [] )
: ccconfig['status_view_selectedHistograms'] || []);
// Note: nodeMetrics are requested on configured $timestep resolution // Note: nodeMetrics are requested on configured $timestep resolution
const metricStatusQuery = $derived(queryStore({ const metricStatusQuery = $derived(queryStore({
client: client, client: client,
@@ -73,43 +74,16 @@
} }
`, `,
variables: { variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster}}, {startTime: { from, to }}],
selectedHistograms: selectedHistograms, selectedHistograms: selectedHistograms,
}, },
})); }));
/* Functions */
// TODO: Originally Uses User View Selection! -> Change to Status View : Adapt Mutations from TopUserSelect
// function updateTopUserConfiguration(select) {
// if (ccconfig[`status_view_selectedHistograms:${cluster}`] != select) {
// updateConfigurationMutation({
// name: `status_view_selectedHistograms:${cluster}`,
// value: JSON.stringify(select),
// }).subscribe((res) => {
// if (res.fetching === false && res.error) {
// throw res.error;
// }
// });
// }
// };
// const updateConfigurationMutation = ({ name, value }) => {
// return mutationStore({
// client: client,
// query: gql`
// mutation ($name: String!, $value: String!) {
// updateConfiguration(name: $name, value: $value)
// }
// `,
// variables: { name, value },
// });
// };
</script> </script>
<!-- Loading indicators & Metric Sleect --> <!-- Loading indicators & Metric Sleect -->
<Row cols={{ xs: 1 }}> <Row class="justify-content-between">
<Col class="text-md-end"> <Col class="mb-2 mb-md-0" xs="12" md="5" lg="4" xl="3">
<Button <Button
outline outline
color="secondary" color="secondary"
@@ -118,7 +92,17 @@
<Icon name="bar-chart-line" /> Select Histograms <Icon name="bar-chart-line" /> Select Histograms
</Button> </Button>
</Col> </Col>
<Col xs="12" md="5" lg="4" xl="3">
<Refresher
initially={120}
onRefresh={() => {
from = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)); // Triggers GQL
to = new Date(Date.now());
}}
/>
</Col>
</Row> </Row>
<Row cols={1} class="text-center mt-3"> <Row cols={1} class="text-center mt-3">
<Col> <Col>
{#if $initq.fetching || $metricStatusQuery.fetching} {#if $initq.fetching || $metricStatusQuery.fetching}
@@ -168,6 +152,7 @@
{cluster} {cluster}
bind:isOpen={isHistogramSelectionOpen} bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms} presetSelectedHistograms={selectedHistograms}
configName="status_view_selectedHistograms"
applyChange={(newSelection) => { applyChange={(newSelection) => {
selectedHistograms = [...newSelection]; selectedHistograms = [...newSelection];
}} }}

View File

@@ -2,7 +2,7 @@
@component Main cluster status view component; renders current system-usage information @component Main cluster status view component; renders current system-usage information
Properties: Properties:
- `cluster String`: The cluster to show status information for - `presetCluster String`: The cluster to show status information for
--> -->
<script> <script>
@@ -26,12 +26,13 @@
init, init,
} from "../generic/utils.js"; } from "../generic/utils.js";
import { scaleNumbers, formatTime } from "../generic/units.js"; import { scaleNumbers, formatTime } from "../generic/units.js";
import Refresher from "../generic/helper/Refresher.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";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
cluster, presetCluster,
useCbColors = false, useCbColors = false,
useAltColors = false, useAltColors = false,
} = $props(); } = $props();
@@ -41,10 +42,11 @@
const client = getContextClient(); const client = getContextClient();
/* State Init */ /* State Init */
let from = $state(new Date(Date.now() - 5 * 60 * 1000)); let cluster = $state(presetCluster);
let to = $state(new Date(Date.now()));
let pieWidth = $state(0); let pieWidth = $state(0);
let plotWidths = $state([]); let plotWidths = $state([]);
let from = $state(new Date(Date.now() - 5 * 60 * 1000));
let to = $state(new Date(Date.now()));
// Bar Gauges // Bar Gauges
let allocatedNodes = $state({}); let allocatedNodes = $state({});
let allocatedAccs = $state({}); let allocatedAccs = $state({});
@@ -232,7 +234,6 @@
}); });
/* Const Functions */ /* Const Functions */
// New: Sum Up Node Averages
const sumUp = (data, subcluster, metric) => const sumUp = (data, subcluster, metric) =>
data.reduce( data.reduce(
(sum, node) => (sum, node) =>
@@ -246,22 +247,6 @@
0, 0,
); );
// Old: SumUp Metric Time Data
// const sumUp = (data, subcluster, metric) =>
// data.reduce(
// (sum, node) =>
// node.subCluster == subcluster
// ? sum +
// (node.metrics
// .find((m) => m.name == metric)
// ?.metric.series.reduce(
// (sum, series) => sum + series.data[series.data.length - 1],
// 0,
// ) || 0)
// : sum,
// 0,
// );
/* Functions */ /* Functions */
function transformJobsStatsToData(subclusterData) { function transformJobsStatsToData(subclusterData) {
/* c will contain values from 0 to 1 representing the duration */ /* c will contain values from 0 to 1 representing the duration */
@@ -311,8 +296,7 @@
let intensity = f / m let intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity)) { if (Number.isNaN(intensity) || !Number.isFinite(intensity)) {
// continue // Old: Introduces mismatch between Data and Info Arrays intensity = 0.0 // Set to Float Zero: Will not show in Log-Plot (Always below render limit)
intensity = 0.0 // New: Set to Float Zero: Will not show in Log-Plot (Always below render limit)
} }
x.push(intensity) x.push(intensity)
@@ -342,8 +326,6 @@
if (subClusterData) { // && $nodesState?.data) { if (subClusterData) { // && $nodesState?.data) {
// Use Nodes as Returned from CCMS, *NOT* as saved in DB via SlurmState-API! // Use Nodes as Returned from CCMS, *NOT* as saved in DB via SlurmState-API!
for (let j = 0; j < subClusterData.length; j++) { for (let j = 0; j < subClusterData.length; j++) {
// nodesCounts[subClusterData[i].subCluster] = $nodesState.data.nodes.count; // Probably better as own derived!
const nodeName = subClusterData[j]?.host ? subClusterData[j].host : "unknown" const nodeName = subClusterData[j]?.host ? subClusterData[j].host : "unknown"
const nodeMatch = $statusQuery?.data?.nodes?.items?.find((n) => n.hostname == nodeName && n.subCluster == subClusterData[j].subCluster); const nodeMatch = $statusQuery?.data?.nodes?.items?.find((n) => n.hostname == nodeName && n.subCluster == subClusterData[j].subCluster);
const nodeState = nodeMatch?.nodeState ? nodeMatch.nodeState : "notindb" const nodeState = nodeMatch?.nodeState ? nodeMatch.nodeState : "notindb"
@@ -375,6 +357,21 @@
</script> </script>
<!-- Refresher and space for other options -->
<Row class="justify-content-end">
<Col xs="12" md="5" lg="4" xl="3">
<Refresher
initially={120}
onRefresh={() => {
from = new Date(Date.now() - 5 * 60 * 1000);
to = new Date(Date.now());
}}
/>
</Col>
</Row>
<hr/>
<!-- Node Health Pis, later Charts --> <!-- Node Health Pis, later Charts -->
{#if $initq.data && $nodesStateCounts.data} {#if $initq.data && $nodesStateCounts.data}
<Row cols={{ lg: 4, md: 2 , sm: 1}} class="mb-3 justify-content-center"> <Row cols={{ lg: 4, md: 2 , sm: 1}} class="mb-3 justify-content-center">

View File

@@ -2,7 +2,7 @@
@component Main cluster status view component; renders current system-usage information @component Main cluster status view component; renders current system-usage information
Properties: Properties:
- `cluster String`: The cluster to show status information for - `presetCluster String`: The cluster to show status information for
--> -->
<script> <script>
@@ -13,7 +13,10 @@
Card, Card,
Table, Table,
Icon, Icon,
Tooltip Tooltip,
Input,
InputGroup,
InputGroupText
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { import {
queryStore, queryStore,
@@ -28,10 +31,11 @@
} from "../generic/utils.js"; } from "../generic/utils.js";
import Pie, { colors } from "../generic/plots/Pie.svelte"; import Pie, { colors } from "../generic/plots/Pie.svelte";
import Histogram from "../generic/plots/Histogram.svelte"; import Histogram from "../generic/plots/Histogram.svelte";
import Refresher from "../generic/helper/Refresher.svelte";
/* Svelte 5 Props */ /* Svelte 5 Props */
let { let {
cluster, presetCluster,
useCbColors = false, useCbColors = false,
useAltColors = false useAltColors = false
} = $props(); } = $props();
@@ -39,11 +43,16 @@
/* Const Init */ /* Const Init */
const { query: initq } = init(); const { query: initq } = init();
const client = getContextClient(); const client = getContextClient();
const durationBinOptions = ["1m","10m","1h","6h","12h"];
/* State Init */ /* State Init */
let cluster = $state(presetCluster)
let from = $state(new Date(Date.now() - (30 * 24 * 60 * 60 * 1000))); // Simple way to retrigger GQL: Jobs Started last Month
let to = $state(new Date(Date.now()));
let colWidthJobs = $state(0); let colWidthJobs = $state(0);
let colWidthNodes = $state(0); let colWidthNodes = $state(0);
let colWidthAccs = $state(0); let colWidthAccs = $state(0);
let numDurationBins = $state("1h");
/* Derived */ /* Derived */
const topJobsQuery = $derived(queryStore({ const topJobsQuery = $derived(queryStore({
@@ -75,7 +84,7 @@
} }
`, `,
variables: { variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster}}, {startTime: { from, to }}],
paging: { itemsPerPage: 10, page: 1 } // Top 10 paging: { itemsPerPage: 10, page: 1 } // Top 10
}, },
})); }));
@@ -109,7 +118,7 @@
} }
`, `,
variables: { variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster }}, {startTime: { from, to }}],
paging: { itemsPerPage: 10, page: 1 } // Top 10 paging: { itemsPerPage: 10, page: 1 } // Top 10
}, },
})); }));
@@ -143,7 +152,7 @@
} }
`, `,
variables: { variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster }}, {startTime: { from, to }}],
paging: { itemsPerPage: 10, page: 1 } // Top 10 paging: { itemsPerPage: 10, page: 1 } // Top 10
}, },
})); }));
@@ -155,8 +164,9 @@
query ( query (
$filter: [JobFilter!]! $filter: [JobFilter!]!
$selectedHistograms: [String!] $selectedHistograms: [String!]
$numDurationBins: String
) { ) {
jobsStatistics(filter: $filter, metrics: $selectedHistograms) { jobsStatistics(filter: $filter, metrics: $selectedHistograms, numDurationBins: $numDurationBins) {
histDuration { histDuration {
count count
value value
@@ -173,8 +183,9 @@
} }
`, `,
variables: { variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }], filter: [{ state: ["running"] }, { cluster: { eq: cluster }}, {startTime: { from, to }}],
selectedHistograms: [], // No Metrics requested for node hardware stats selectedHistograms: [], // No Metrics requested for node hardware stats
numDurationBins: numDurationBins,
}, },
})); }));
@@ -191,14 +202,46 @@
} }
return c[(c.length + targetIdx) % c.length]; return c[(c.length + targetIdx) % c.length];
} }
</script> </script>
<!-- Refresher and space for other options -->
<Row class="justify-content-between">
<Col class="mb-2 mb-md-0" xs="12" md="5" lg="4" xl="3">
<InputGroup>
<InputGroupText>
<Icon name="bar-chart-line-fill" />
</InputGroupText>
<InputGroupText>
Duration Bin Size
</InputGroupText>
<Input type="select" bind:value={numDurationBins}>
{#each durationBinOptions as dbin}
<option value={dbin}>{dbin}</option>
{/each}
</Input>
</InputGroup>
</Col>
<Col xs="12" md="5" lg="4" xl="3">
<Refresher
initially={120}
onRefresh={() => {
from = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)); // Triggers GQL
to = new Date(Date.now());
}}
/>
</Col>
</Row>
<hr/>
<!-- Job Duration, Top Users and Projects--> <!-- Job Duration, Top Users and Projects-->
{#if $topJobsQuery.fetching || $nodeStatusQuery.fetching} {#if $topJobsQuery.fetching || $nodeStatusQuery.fetching}
<Spinner /> <Spinner />
{:else if $topJobsQuery.data && $nodeStatusQuery.data} {:else if $topJobsQuery.data && $nodeStatusQuery.data}
<Row> <Row>
<Col xs="12" lg="4" class="p-2"> <Col xs="12" lg="4" class="p-2">
{#key $nodeStatusQuery.data.jobsStatistics[0].histDuration}
<Histogram <Histogram
data={convert2uplot($nodeStatusQuery.data.jobsStatistics[0].histDuration)} data={convert2uplot($nodeStatusQuery.data.jobsStatistics[0].histDuration)}
title="Duration Distribution" title="Duration Distribution"
@@ -210,6 +253,7 @@
usesBins usesBins
xtime xtime
/> />
{/key}
</Col> </Col>
<Col xs="6" md="3" lg="2" class="p-2"> <Col xs="6" md="3" lg="2" class="p-2">
<div bind:clientWidth={colWidthJobs}> <div bind:clientWidth={colWidthJobs}>
@@ -233,7 +277,7 @@
<tr class="mb-2"> <tr class="mb-2">
<th></th> <th></th>
<th style="padding-left: 0.5rem;">User</th> <th style="padding-left: 0.5rem;">User</th>
<th>Active Jobs</th> <th>Jobs</th>
</tr> </tr>
{#each $topJobsQuery.data.topUser as tu, i} {#each $topJobsQuery.data.topUser as tu, i}
<tr> <tr>
@@ -276,7 +320,7 @@
<tr class="mb-2"> <tr class="mb-2">
<th></th> <th></th>
<th style="padding-left: 0.5rem;">Project</th> <th style="padding-left: 0.5rem;">Project</th>
<th>Active Jobs</th> <th>Jobs</th>
</tr> </tr>
{#each $topJobsQuery.data.topProjects as tp, i} {#each $topJobsQuery.data.topProjects as tp, i}
<tr> <tr>