reorganize plots, reduce tabs,

This commit is contained in:
Christoph Kluge
2025-08-12 17:04:31 +02:00
parent 10194105e3
commit bd2cdfcef2
6 changed files with 621 additions and 310 deletions

View File

@@ -459,7 +459,7 @@
</tr>
{#each $topQuery.data.topList as te, i}
<tr>
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
<td><Icon name="circle-fill" style="color: {colors['colorblind'][i]};" /></td>
{#if groupSelection.key == "user"}
<th scope="col" id="topName-{te.id}"
><a href="/monitoring/user/{te.id}?cluster={clusterName}"

View File

@@ -6,6 +6,9 @@
-->
<script>
import {
getContext
} from "svelte"
import {
Row,
Col,
@@ -27,6 +30,9 @@
cluster
} = $props();
/*Const Init */
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()));
@@ -69,33 +75,21 @@
<Card class="overflow-auto" style="height: auto;">
<TabContent>
<TabPane tabId="devel-dash" tab="Devel" active>
<TabPane tabId="status-dash" tab="Status" active>
<CardBody>
<DevelDash {cluster}></DevelDash>
</CardBody>
</TabPane>
<TabPane tabId="status-dash" tab="Status">
<CardBody>
<StatusDash {cluster}></StatusDash>
<StatusDash {cluster} {useCbColors} useAltColors></StatusDash>
</CardBody>
</TabPane>
<TabPane tabId="usage-dash" tab="Usage">
<CardBody>
<UsageDash {cluster}></UsageDash>
<UsageDash {cluster} {useCbColors}></UsageDash>
</CardBody>
</TabPane>
<TabPane tabId="node-dash" tab="Nodes">
<CardBody>
<NodeDash {cluster}></NodeDash>
</CardBody>
</TabPane>
<TabPane tabId="metric-dash" tab="Statistics">
<CardBody>
<StatisticsDash {cluster}></StatisticsDash>
<StatisticsDash {cluster} {useCbColors}></StatisticsDash>
</CardBody>
</TabPane>
</TabContent>

View File

@@ -14,55 +14,57 @@
-->
<script module>
// https://www.learnui.design/tools/data-color-picker.html#divergent: 11, Shallow Green-Red
export const colors = [
"#00876c",
"#449c6e",
"#70af6f",
"#9bc271",
"#c8d377",
"#f7e382",
"#f6c468",
"#f3a457",
"#ed834e",
"#e3614d",
"#d43d51",
];
// https://www.learnui.design/tools/data-color-picker.html#palette: 12, Colorwheel-Like
export const altColors = [
"#0022bb",
"#ba0098",
"#fa0066",
"#ff6234",
"#ffae00",
"#b1af00",
"#67a630",
"#009753",
"#00836c",
"#006d77",
"#005671",
"#003f5c",
]
// http://tsitsul.in/blog/coloropt/ : 12 colors normal
export const cbColors = [
'rgb(235,172,35)',
'rgb(184,0,88)',
'rgb(0,140,249)',
'rgb(0,110,0)',
'rgb(0,187,173)',
'rgb(209,99,230)',
'rgb(178,69,2)',
'rgb(255,146,135)',
'rgb(89,84,214)',
'rgb(0,198,248)',
'rgb(135,133,0)',
'rgb(0,167,108)',
'rgb(189,189,189)'
];
export const colors = {
// https://www.learnui.design/tools/data-color-picker.html#divergent: 11, Shallow Green-Red
default: [
"#00876c",
"#449c6e",
"#70af6f",
"#9bc271",
"#c8d377",
"#f7e382",
"#f6c468",
"#f3a457",
"#ed834e",
"#e3614d",
"#d43d51",
],
// https://www.learnui.design/tools/data-color-picker.html#palette: 12, Colorwheel-Like
alternative: [
"#0022bb",
"#ba0098",
"#fa0066",
"#ff6234",
"#ffae00",
"#b1af00",
"#67a630",
"#009753",
"#00836c",
"#006d77",
"#005671",
"#003f5c",
],
// http://tsitsul.in/blog/coloropt/ : 12 colors normal
colorblind: [
'rgb(235,172,35)',
'rgb(184,0,88)',
'rgb(0,140,249)',
'rgb(0,110,0)',
'rgb(0,187,173)',
'rgb(209,99,230)',
'rgb(178,69,2)',
'rgb(255,146,135)',
'rgb(89,84,214)',
'rgb(0,198,248)',
'rgb(135,133,0)',
'rgb(0,167,108)',
'rgb(189,189,189)',
]
}
</script>
<script>
/* Ignore Double Script Section Error in IDE */
// Ignore VSC IDE "One Instance Level Script" Error
import { onMount, getContext } from "svelte";
import Chart from 'chart.js/auto';
@@ -74,10 +76,11 @@
quantities,
entities,
displayLegend = false,
useAltColors = false,
} = $props();
/* Const Init */
const ccconfig = getContext("cc-config");
const useCbColors = getContext("cc-config")?.plot_general_colorblindMode || false
const options = {
maintainAspectRatio: false,
animation: false,
@@ -88,10 +91,19 @@
}
};
/* State Init */
let cbmode = $state(ccconfig?.plot_general_colorblindMode || false)
/* Derived */
const colorPalette = $derived.by(() => {
let c;
if (useCbColors) {
c = [...colors['colorblind']];
} else if (useAltColors) {
c = [...colors['alternative']];
} else {
c = [...colors['default']];
}
return c.slice(0, quantities.length);
})
const data = $derived({
labels: entities,
datasets: [
@@ -99,7 +111,7 @@
label: sliceLabel,
data: quantities,
fill: 1,
backgroundColor: cbmode ? cbColors.slice(0, quantities.length) : colors.slice(0, quantities.length)
backgroundColor: colorPalette,
}
]
});
@@ -118,7 +130,7 @@
</script>
<!-- <div style="width: 500px;"><canvas id="dimensions"></canvas></div><br/> -->
<div class="chart-container" style="--container-width: {size}; --container-height: {size}">
<div class="chart-container" style="--container-width: {size}px; --container-height: {size}px">
<canvas id={canvasId}></canvas>
</div>

View File

@@ -22,12 +22,13 @@
} from "../generic/utils.js";
//import Roofline from "../generic/plots/Roofline.svelte";
import Roofline from "../generic/plots/Roofline.svelte";
import Pie, { cbColors, colors } from "../generic/plots/Pie.svelte";
import Pie, { colors } from "../generic/plots/Pie.svelte";
import { formatTime } from "../generic/units.js";
/* Svelte 5 Props */
let {
cluster
cluster,
useCbColors = false
} = $props();
/* Const Init */
@@ -40,7 +41,6 @@
let plotWidths = $state([]);
let statesWidth = $state(0);
let healthWidth = $state(0);
let cbmode = $state(false);
// let nodesCounts = $state({});
// let jobsJounts = $state({});
@@ -313,6 +313,12 @@
return result
}
function legendColors(targetIdx) {
// Reuses first color if targetIdx overflows
let c = [...colors['default']];
return c[(c.length + targetIdx) % c.length];
}
</script>
<!-- Gauges & Roofline per Subcluster-->
@@ -386,7 +392,7 @@
}, 0)} Nodes
</b>
<Pie
canvasId="hpcpie-slurm"
canvasId="hpcpie-slurm-old"
size={statesWidth}
sliceLabel="Nodes"
quantities={refinedStateData.map(
@@ -409,7 +415,7 @@
</tr>
{#each refinedStateData as sd, i}
<tr>
<td><Icon name="circle-fill" style="color: {colors[i]};" /></td>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td>{sd.state}</td>
<td>{sd.count}</td>
</tr>
@@ -427,7 +433,7 @@
}, 0)} Nodes
</b>
<Pie
canvasId="hpcpie-health"
canvasId="hpcpie-health-old"
size={healthWidth}
sliceLabel="Nodes"
quantities={refinedHealthData.map(
@@ -450,7 +456,7 @@
</tr>
{#each refinedHealthData as hd, i}
<tr>
<td><Icon name="circle-fill" style="color: {cbmode ? cbColors[i] : colors[i]};" /></td>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td>{hd.state}</td>
<td>{hd.count}</td>
</tr>

View File

@@ -15,7 +15,7 @@
CardBody,
Table,
Progress,
// Icon,
Icon,
} from "@sveltestrap/sveltestrap";
import {
queryStore,
@@ -24,15 +24,16 @@
} from "@urql/svelte";
import {
init,
// transformPerNodeDataForRoofline,
} from "../generic/utils.js";
import { scaleNumbers, formatTime } from "../generic/units.js";
import Roofline from "../generic/plots/Roofline.svelte";
import Pie, { colors } from "../generic/plots/Pie.svelte";
/* Svelte 5 Props */
let {
cluster
cluster,
useCbColors = false,
useAltColors = false,
} = $props();
/* Const Init */
@@ -42,6 +43,7 @@
/* State Init */
let from = $state(new Date(Date.now() - 5 * 60 * 1000));
let to = $state(new Date(Date.now()));
let pieWidth = $state(0);
let plotWidths = $state([]);
// Bar Gauges
let allocatedNodes = $state({});
@@ -58,6 +60,30 @@
let totalAccs = $state({});
/* Derived */
// Accumulated NodeStates for Piecharts
const nodesStateCounts = $derived(queryStore({
client: client,
query: gql`
query ($filter: [NodeFilter!]) {
nodeStates(filter: $filter) {
state
count
}
}
`,
variables: {
filter: { cluster: { eq: cluster }}
},
}));
const refinedStateData = $derived.by(() => {
return $nodesStateCounts?.data?.nodeStates.filter((e) => ['allocated', 'reserved', 'idle', 'mixed','down', 'unknown'].includes(e.state))
});
const refinedHealthData = $derived.by(() => {
return $nodesStateCounts?.data?.nodeStates.filter((e) => ['full', 'partial', 'failed'].includes(e.state))
});
// Note: nodeMetrics are requested on configured $timestep resolution
// Result: The latest 5 minutes (datapoints) for each node independent of job
const statusQuery = $derived(queryStore({
@@ -334,8 +360,107 @@
return result
}
function legendColors(targetIdx) {
// Reuses first color if targetIdx overflows
let c;
if (useCbColors) {
c = [...colors['colorblind']];
} else if (useAltColors) {
c = [...colors['alternative']];
} else {
c = [...colors['default']];
}
return c[(c.length + targetIdx) % c.length];
}
</script>
<!-- Node Health Pis, later Charts -->
{#if $initq.data && $nodesStateCounts.data}
<Row cols={{ lg: 4, md: 2 , sm: 1}} class="mb-3 justify-content-center">
<Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={pieWidth}>
{#key refinedStateData}
<h4 class="text-center">
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States
</h4>
<Pie
{useAltColors}
canvasId="hpcpie-slurm"
size={pieWidth * 0.55}
sliceLabel="Nodes"
quantities={refinedStateData.map(
(sd) => sd.count,
)}
entities={refinedStateData.map(
(sd) => sd.state,
)}
/>
{/key}
</div>
</Col>
<Col class="px-4 py-2">
{#key refinedStateData}
<Table>
<tr class="mb-2">
<th></th>
<th>Current State</th>
<th>Nodes</th>
</tr>
{#each refinedStateData as sd, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColors(i)};"/></td>
<td>{sd.state}</td>
<td>{sd.count}</td>
</tr>
{/each}
</Table>
{/key}
</Col>
<Col class="px-3 mt-2 mt-lg-0">
<div bind:clientWidth={pieWidth}>
{#key refinedHealthData}
<h4 class="text-center">
{cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node Health
</h4>
<Pie
{useAltColors}
canvasId="hpcpie-health"
size={pieWidth * 0.55}
sliceLabel="Nodes"
quantities={refinedHealthData.map(
(sd) => sd.count,
)}
entities={refinedHealthData.map(
(sd) => sd.state,
)}
/>
{/key}
</div>
</Col>
<Col class="px-4 py-2">
{#key refinedHealthData}
<Table>
<tr class="mb-2">
<th></th>
<th>Current Health</th>
<th>Nodes</th>
</tr>
{#each refinedHealthData as hd, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td>{hd.state}</td>
<td>{hd.count}</td>
</tr>
{/each}
</Table>
{/key}
</Col>
</Row>
{/if}
<hr/>
<!-- Gauges & Roofline per Subcluster-->
{#if $initq.data && $statusQuery.data}
{#each $initq.data.clusters.find((c) => c.name == cluster).subClusters as subCluster, i}
@@ -454,5 +579,5 @@
</Row>
{/each}
{:else}
<Card class="mx-4" body color="warning">Cannot render status tab: No data!</Card>
<Card class="mx-4" body color="warning">Cannot render status rooflines: No data!</Card>
{/if}

View File

@@ -6,7 +6,6 @@
-->
<script>
import { getContext } from "svelte";
import {
Row,
Col,
@@ -20,310 +19,485 @@
queryStore,
gql,
getContextClient,
mutationStore,
} from "@urql/svelte";
import {
init,
scramble,
scrambleNames,
convert2uplot,
} from "../generic/utils.js";
import Pie, { colors, cbColors } from "../generic/plots/Pie.svelte";
import Pie, { colors } from "../generic/plots/Pie.svelte";
import Histogram from "../generic/plots/Histogram.svelte";
/* Svelte 5 Props */
let {
cluster
cluster,
useCbColors = false,
useAltColors = false
} = $props();
/* Const Init */
const { query: initq } = init();
const ccconfig = getContext("cc-config");
const client = getContextClient();
const paging = { itemsPerPage: 10, page: 1 }; // Top 10
const topOptions = [
{ key: "totalJobs", label: "Jobs" },
{ key: "totalNodes", label: "Nodes" },
{ key: "totalCores", label: "Cores" },
{ key: "totalAccs", label: "Accelerators" },
];
/* State Init */
let colWidth = $state(0);
let cbmode = $state(ccconfig?.plot_general_colorblindMode || false)
// Pie Charts
let topProjectSelection = $state(
topOptions.find(
(option) =>
option.key ==
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`],
) ||
topOptions.find(
(option) => option.key == ccconfig.status_view_selectedTopProjectCategory,
)
);
let topUserSelection = $state(
topOptions.find(
(option) =>
option.key ==
ccconfig[`status_view_selectedTopUserCategory:${cluster}`],
) ||
topOptions.find(
(option) => option.key == ccconfig.status_view_selectedTopUserCategory,
)
);
let colWidthJobs = $state(0);
let colWidthNodes = $state(0);
let colWidthAccs = $state(0);
/* Derived */
const topUserQuery = $derived(queryStore({
const topJobsQuery = $derived(queryStore({
client: client,
query: gql`
query (
$filter: [JobFilter!]!
$paging: PageRequest!
$sortBy: SortByAggregate!
) {
topUser: jobsStatistics(
filter: $filter
page: $paging
sortBy: $sortBy
sortBy: TOTALJOBS
groupBy: USER
) {
id
name
totalJobs
totalNodes
totalCores
totalAccs
}
topProjects: jobsStatistics(
filter: $filter
page: $paging
sortBy: TOTALJOBS
groupBy: PROJECT
) {
id
totalJobs
}
}
`,
variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
paging,
sortBy: topUserSelection.key.toUpperCase(),
paging: { itemsPerPage: 10, page: 1 } // Top 10
},
}));
const topProjectQuery = $derived(queryStore({
const topNodesQuery = $derived(queryStore({
client: client,
query: gql`
query (
$filter: [JobFilter!]!
$paging: PageRequest!
$sortBy: SortByAggregate!
) {
topUser: jobsStatistics(
filter: $filter
page: $paging
sortBy: TOTALNODES
groupBy: USER
) {
id
name
totalNodes
}
topProjects: jobsStatistics(
filter: $filter
page: $paging
sortBy: $sortBy
sortBy: TOTALNODES
groupBy: PROJECT
) {
id
totalJobs
totalNodes
totalCores
}
}
`,
variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
paging: { itemsPerPage: 10, page: 1 } // Top 10
},
}));
const topAccsQuery = $derived(queryStore({
client: client,
query: gql`
query (
$filter: [JobFilter!]!
$paging: PageRequest!
) {
topUser: jobsStatistics(
filter: $filter
page: $paging
sortBy: TOTALACCS
groupBy: USER
) {
id
name
totalAccs
}
topProjects: jobsStatistics(
filter: $filter
page: $paging
sortBy: TOTALACCS
groupBy: PROJECT
) {
id
totalAccs
}
}
`,
variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
paging,
sortBy: topProjectSelection.key.toUpperCase(),
paging: { itemsPerPage: 10, page: 1 } // Top 10
},
}));
/* Effects */
$effect(() => {
updateTopUserConfiguration(topUserSelection.key);
});
$effect(() => {
updateTopProjectConfiguration(topProjectSelection.key);
});
/* Const Functions */
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
// Note: nodeMetrics are requested on configured $timestep resolution
const nodeStatusQuery = $derived(queryStore({
client: client,
query: gql`
query (
$filter: [JobFilter!]!
$selectedHistograms: [String!]
) {
jobsStatistics(filter: $filter, metrics: $selectedHistograms) {
histDuration {
count
value
}
histNumNodes {
count
value
}
histNumAccs {
count
value
}
}
`,
variables: { name, value },
});
};
}
`,
variables: {
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
selectedHistograms: [], // No Metrics requested for node hardware stats
},
}));
/* Functions */
function updateTopUserConfiguration(select) {
if (ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select) {
updateConfigurationMutation({
name: `status_view_selectedTopUserCategory:${cluster}`,
value: JSON.stringify(select),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error;
}
});
}
}
function updateTopProjectConfiguration(select) {
if (
ccconfig[`status_view_selectedTopProjectCategory:${cluster}`] != select
) {
updateConfigurationMutation({
name: `status_view_selectedTopProjectCategory:${cluster}`,
value: JSON.stringify(select),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error;
}
});
}
}
function legendColor(targetIdx) {
function legendColors(targetIdx) {
// Reuses first color if targetIdx overflows
if (cbmode) {
return cbColors[(colors.length + targetIdx) % colors.length]
} else {
return colors[(colors.length + targetIdx) % colors.length]
}
let c;
if (useCbColors) {
c = [...colors['colorblind']];
} else if (useAltColors) {
c = [...colors['alternative']];
} else {
c = [...colors['default']];
}
return c[(c.length + targetIdx) % c.length];
}
</script>
{#if $initq.data}
<!-- User and Project Stats as Pie-Charts -->
<Row cols={{ lg: 4, md: 2, sm: 1 }}>
<Col class="p-2">
<div bind:clientWidth={colWidth}>
<!-- Job Duration, Top Users and Projects-->
{#if $topJobsQuery.fetching || $nodeStatusQuery.fetching}
<Spinner />
{:else if $topJobsQuery.data && $nodeStatusQuery.data}
<Row>
<Col xs="4" class="p-2">
<Histogram
data={convert2uplot($nodeStatusQuery.data.jobsStatistics[0].histDuration)}
title="Duration Distribution"
xlabel="Current Job Runtimes"
xunit="Runtime"
ylabel="Number of Jobs"
yunit="Jobs"
height="275"
usesBins
xtime
/>
</Col>
<Col xs="2" class="p-2">
<div bind:clientWidth={colWidthJobs}>
<h4 class="text-center">
Top Users on {cluster.charAt(0).toUpperCase() + cluster.slice(1)}
Top Users: Jobs
</h4>
{#key $topUserQuery.data}
{#if $topUserQuery.fetching}
<Spinner />
{:else if $topUserQuery.error}
<Card body color="danger">{$topUserQuery.error.message}</Card>
{:else}
<Pie
canvasId="hpcpie-users"
size={colWidth}
sliceLabel={topUserSelection.label}
quantities={$topUserQuery.data.topUser.map(
(tu) => tu[topUserSelection.key],
)}
entities={$topUserQuery.data.topUser.map((tu) => scrambleNames ? scramble(tu.id) : tu.id)}
/>
{/if}
{/key}
<Pie
{useAltColors}
canvasId="hpcpie-jobs-users"
size={colWidthJobs * 0.75}
sliceLabel="Jobs"
quantities={$topJobsQuery.data.topUser.map(
(tu) => tu['totalJobs'],
)}
entities={$topJobsQuery.data.topUser.map((tu) => scrambleNames ? scramble(tu.id) : tu.id)}
/>
</div>
</Col>
<Col class="px-4 py-2">
{#key $topUserQuery.data}
{#if $topUserQuery.fetching}
<Spinner />
{:else if $topUserQuery.error}
<Card body color="danger">{$topUserQuery.error.message}</Card>
{:else}
<Table>
<tr class="mb-2">
<th>Legend</th>
<th>User Name</th>
<th
>Number of
<select class="p-0" bind:value={topUserSelection}>
{#each topOptions as option}
<option value={option}>
{option.label}
</option>
{/each}
</select>
</th>
</tr>
{#each $topUserQuery.data.topUser as tu, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColor(i)};" /></td>
<th scope="col" id="topName-{tu.id}"
><a
href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
>{scrambleNames ? scramble(tu.id) : tu.id}</a
></th
>
{#if tu?.name}
<Tooltip
target={`topName-${tu.id}`}
placement="left"
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
>
{/if}
<td>{tu[topUserSelection.key]}</td>
</tr>
{/each}
</Table>
{/if}
{/key}
<Col xs="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">User</th>
<th>Active Jobs</th>
</tr>
{#each $topJobsQuery.data.topUser as tu, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td id="topName-jobs-{tu.id}">
<a target="_blank" href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
>{scrambleNames ? scramble(tu.id) : tu.id}
</a>
</td>
{#if tu?.name}
<Tooltip
target={`topName-jobs-${tu.id}`}
placement="left"
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
>
{/if}
<td>{tu['totalJobs']}</td>
</tr>
{/each}
</Table>
</Col>
<Col class="p-2">
<Col xs="2" class="p-2">
<h4 class="text-center">
Top Projects on {cluster.charAt(0).toUpperCase() + cluster.slice(1)}
Top Projects: Jobs
</h4>
{#key $topProjectQuery.data}
{#if $topProjectQuery.fetching}
<Spinner />
{:else if $topProjectQuery.error}
<Card body color="danger">{$topProjectQuery.error.message}</Card>
{:else}
<Pie
canvasId="hpcpie-projects"
size={colWidth}
sliceLabel={topProjectSelection.label}
quantities={$topProjectQuery.data.topProjects.map(
(tp) => tp[topProjectSelection.key],
)}
entities={$topProjectQuery.data.topProjects.map((tp) => scrambleNames ? scramble(tp.id) : tp.id)}
/>
{/if}
{/key}
<Pie
{useAltColors}
canvasId="hpcpie-jobs-projects"
size={colWidthJobs * 0.75}
sliceLabel={'Jobs'}
quantities={$topJobsQuery.data.topProjects.map(
(tp) => tp['totalJobs'],
)}
entities={$topJobsQuery.data.topProjects.map((tp) => scrambleNames ? scramble(tp.id) : tp.id)}
/>
</Col>
<Col class="px-4 py-2">
{#key $topProjectQuery.data}
{#if $topProjectQuery.fetching}
<Spinner />
{:else if $topProjectQuery.error}
<Card body color="danger">{$topProjectQuery.error.message}</Card>
{:else}
<Table>
<tr class="mb-2">
<th>Legend</th>
<th>Project Code</th>
<th
>Number of
<select class="p-0" bind:value={topProjectSelection}>
{#each topOptions as option}
<option value={option}>
{option.label}
</option>
{/each}
</select>
</th>
</tr>
{#each $topProjectQuery.data.topProjects as tp, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColor(i)};" /></td>
<th scope="col"
><a
href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
>{scrambleNames ? scramble(tp.id) : tp.id}</a
></th
>
<td>{tp[topProjectSelection.key]}</td>
</tr>
{/each}
</Table>
{/if}
{/key}
<Col xs="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">Project</th>
<th>Active Jobs</th>
</tr>
{#each $topJobsQuery.data.topProjects as tp, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td>
<a target="_blank" href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
>{scrambleNames ? scramble(tp.id) : tp.id}
</a>
</td>
<td>{tp['totalJobs']}</td>
</tr>
{/each}
</Table>
</Col>
</Row>
{:else}
<Card class="mx-4" body color="warning">Cannot render job status charts: No data!</Card>
{/if}
<hr/>
<!-- Node Distribution, Top Users and Projects-->
{#if $topNodesQuery.fetching || $nodeStatusQuery.fetching}
<Spinner />
{:else if $topNodesQuery.data && $nodeStatusQuery.data}
<Row>
<Col xs="4" class="p-2">
<Histogram
data={convert2uplot($nodeStatusQuery.data.jobsStatistics[0].histNumNodes)}
title="Number of Nodes Distribution"
xlabel="Allocated Nodes"
xunit="Nodes"
ylabel="Number of Jobs"
yunit="Jobs"
height="275"
/>
</Col>
<Col xs="2" class="p-2">
<div bind:clientWidth={colWidthNodes}>
<h4 class="text-center">
Top Users: Nodes
</h4>
<Pie
{useAltColors}
canvasId="hpcpie-nodes-users"
size={colWidthNodes * 0.75}
sliceLabel="Nodes"
quantities={$topNodesQuery.data.topUser.map(
(tu) => tu['totalNodes'],
)}
entities={$topNodesQuery.data.topUser.map((tu) => scrambleNames ? scramble(tu.id) : tu.id)}
/>
</div>
</Col>
<Col xs="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">User</th>
<th>Nodes</th>
</tr>
{#each $topNodesQuery.data.topUser as tu, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td id="topName-nodes-{tu.id}">
<a target="_blank" href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
>{scrambleNames ? scramble(tu.id) : tu.id}
</a>
</td>
{#if tu?.name}
<Tooltip
target={`topName-nodes-${tu.id}`}
placement="left"
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
>
{/if}
<td>{tu['totalNodes']}</td>
</tr>
{/each}
</Table>
</Col>
<Col xs="2" class="p-2">
<h4 class="text-center">
Top Projects: Nodes
</h4>
<Pie
{useAltColors}
canvasId="hpcpie-nodes-projects"
size={colWidthNodes * 0.75}
sliceLabel={'Nodes'}
quantities={$topNodesQuery.data.topProjects.map(
(tp) => tp['totalNodes'],
)}
entities={$topNodesQuery.data.topProjects.map((tp) => scrambleNames ? scramble(tp.id) : tp.id)}
/>
</Col>
<Col xs="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">Project</th>
<th>Nodes</th>
</tr>
{#each $topNodesQuery.data.topProjects as tp, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td>
<a target="_blank" href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
>{scrambleNames ? scramble(tp.id) : tp.id}
</a>
</td>
<td>{tp['totalNodes']}</td>
</tr>
{/each}
</Table>
</Col>
</Row>
{:else}
<Card class="mx-4" body color="warning">Cannot render node status charts: No data!</Card>
{/if}
<hr/>
<!-- Acc Distribution, Top Users and Projects-->
{#if $topAccsQuery.fetching || $nodeStatusQuery.fetching}
<Spinner />
{:else if $topAccsQuery.data && $nodeStatusQuery.data}
<Row>
<Col xs="4" class="p-2">
<Histogram
data={convert2uplot($nodeStatusQuery.data.jobsStatistics[0].histNumAccs)}
title="Number of Accelerators Distribution"
xlabel="Allocated Accs"
xunit="Accs"
ylabel="Number of Jobs"
yunit="Jobs"
height="275"
/>
</Col>
<Col xs="2" class="p-2">
<div bind:clientWidth={colWidthAccs}>
<h4 class="text-center">
Top Users: GPUs
</h4>
<Pie
{useAltColors}
canvasId="hpcpie-accs-users"
size={colWidthAccs * 0.75}
sliceLabel="GPUs"
quantities={$topAccsQuery.data.topUser.map(
(tu) => tu['totalAccs'],
)}
entities={$topAccsQuery.data.topUser.map((tu) => scrambleNames ? scramble(tu.id) : tu.id)}
/>
</div>
</Col>
<Col xs="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">User</th>
<th>GPUs</th>
</tr>
{#each $topAccsQuery.data.topUser as tu, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td id="topName-accs-{tu.id}">
<a target="_blank" href="/monitoring/user/{tu.id}?cluster={cluster}&state=running"
>{scrambleNames ? scramble(tu.id) : tu.id}
</a>
</td>
{#if tu?.name}
<Tooltip
target={`topName-accs-${tu.id}`}
placement="left"
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
>
{/if}
<td>{tu['totalAccs']}</td>
</tr>
{/each}
</Table>
</Col>
<Col xs="2" class="p-2">
<h4 class="text-center">
Top Projects: GPUs
</h4>
<Pie
{useAltColors}
canvasId="hpcpie-accs-projects"
size={colWidthAccs * 0.75}
sliceLabel={'GPUs'}
quantities={$topAccsQuery.data.topProjects.map(
(tp) => tp['totalAccs'],
)}
entities={$topAccsQuery.data.topProjects.map((tp) => scrambleNames ? scramble(tp.id) : tp.id)}
/>
</Col>
<Col xs="2" class="p-2">
<Table>
<tr class="mb-2">
<th></th>
<th style="padding-left: 0.5rem;">Project</th>
<th>GPUs</th>
</tr>
{#each $topAccsQuery.data.topProjects as tp, i}
<tr>
<td><Icon name="circle-fill" style="color: {legendColors(i)};" /></td>
<td>
<a target="_blank" href="/monitoring/jobs/?cluster={cluster}&state=running&project={tp.id}&projectMatch=eq"
>{scrambleNames ? scramble(tp.id) : tp.id}
</a>
</td>
<td>{tp['totalAccs']}</td>
</tr>
{/each}
</Table>
</Col>
</Row>
{:else}
<Card class="mx-4" body color="warning">Cannot render accelerator status charts: No data!</Card>
{/if}