mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-03-15 04:17:30 +01:00
581 lines
17 KiB
Svelte
581 lines
17 KiB
Svelte
<!--
|
|
@component Main cluster status view component; renders current system-usage information
|
|
|
|
Properties:
|
|
- `presetCluster String`: The cluster to show status information for
|
|
- `presetSubCluster String?`: The subCluster to show status information for [Default: null]
|
|
- `useCbColors Bool?`: Use colorblind friendly colors [Default: false]
|
|
- `useAltColors Bool?`: Use alternative color set [Default: false]
|
|
-->
|
|
|
|
<script>
|
|
import {
|
|
Row,
|
|
Col,
|
|
Spinner,
|
|
Card,
|
|
Table,
|
|
Icon,
|
|
Tooltip,
|
|
Input,
|
|
InputGroup,
|
|
InputGroupText,
|
|
} from "@sveltestrap/sveltestrap";
|
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
|
import {
|
|
scramble,
|
|
scrambleNames,
|
|
convert2uplot,
|
|
} from "../../generic/utils.js";
|
|
import Pie, { colors } from "../../generic/plots/Pie.svelte";
|
|
import Histogram from "../../generic/plots/Histogram.svelte";
|
|
import Refresher from "../../generic/helper/Refresher.svelte";
|
|
|
|
/* Svelte 5 Props */
|
|
let {
|
|
presetCluster,
|
|
presetSubCluster = null,
|
|
useCbColors = false,
|
|
useAltColors = false,
|
|
loadMe = false,
|
|
} = $props();
|
|
|
|
/* Const Init */
|
|
const client = getContextClient();
|
|
const durationBinOptions = ["1m", "10m", "1h", "6h", "12h"];
|
|
|
|
/* State Init */
|
|
let pagingState = $state({ page: 1, itemsPerPage: 10 }); // Top 10
|
|
let selectedHistograms = $state([]); // Dummy For Refresh
|
|
let colWidthJobs = $state(0);
|
|
let colWidthNodes = $state(0);
|
|
let colWidthAccs = $state(0);
|
|
let numDurationBins = $state("1h");
|
|
|
|
/* Derived */
|
|
const canvasPrefix = $derived(
|
|
`${presetCluster}-${presetSubCluster ? presetSubCluster : ""}`,
|
|
);
|
|
|
|
const statusFilter = $derived(
|
|
presetSubCluster
|
|
? [
|
|
{ cluster: { eq: presetCluster } },
|
|
{ subCluster: { eq: presetSubCluster } },
|
|
{ state: ["running"] },
|
|
]
|
|
: [{ cluster: { eq: presetCluster } }, { state: ["running"] }],
|
|
);
|
|
const topStatsQuery = $derived(
|
|
loadMe
|
|
? queryStore({
|
|
client: client,
|
|
query: gql`
|
|
query ($filter: [JobFilter!]!) {
|
|
allUsers: jobsStatistics(
|
|
filter: $filter
|
|
groupBy: USER
|
|
) {
|
|
id
|
|
name
|
|
totalJobs
|
|
totalNodes
|
|
totalAccs
|
|
}
|
|
allProjects: jobsStatistics(
|
|
filter: $filter
|
|
groupBy: PROJECT
|
|
) {
|
|
id
|
|
totalJobs
|
|
totalNodes
|
|
totalAccs
|
|
}
|
|
}
|
|
`,
|
|
variables: {
|
|
filter: statusFilter,
|
|
},
|
|
requestPolicy: "network-only",
|
|
})
|
|
: null,
|
|
);
|
|
|
|
// Sort + slice top-10 from the full results in the frontend
|
|
const topUserJobs = $derived(
|
|
$topStatsQuery?.data?.allUsers
|
|
?.toSorted((a, b) => b.totalJobs - a.totalJobs)
|
|
.slice(0, 10) ?? [],
|
|
);
|
|
const topProjectJobs = $derived(
|
|
$topStatsQuery?.data?.allProjects
|
|
?.toSorted((a, b) => b.totalJobs - a.totalJobs)
|
|
.slice(0, 10) ?? [],
|
|
);
|
|
const topUserNodes = $derived(
|
|
$topStatsQuery?.data?.allUsers
|
|
?.toSorted((a, b) => b.totalNodes - a.totalNodes)
|
|
.slice(0, 10) ?? [],
|
|
);
|
|
const topProjectNodes = $derived(
|
|
$topStatsQuery?.data?.allProjects
|
|
?.toSorted((a, b) => b.totalNodes - a.totalNodes)
|
|
.slice(0, 10) ?? [],
|
|
);
|
|
const topUserAccs = $derived(
|
|
$topStatsQuery?.data?.allUsers
|
|
?.toSorted((a, b) => b.totalAccs - a.totalAccs)
|
|
.slice(0, 10) ?? [],
|
|
);
|
|
const topProjectAccs = $derived(
|
|
$topStatsQuery?.data?.allProjects
|
|
?.toSorted((a, b) => b.totalAccs - a.totalAccs)
|
|
.slice(0, 10) ?? [],
|
|
);
|
|
|
|
// Note: nodeMetrics are requested on configured $timestep resolution
|
|
const nodeStatusQuery = $derived(
|
|
loadMe
|
|
? queryStore({
|
|
client: client,
|
|
query: gql`
|
|
query (
|
|
$filter: [JobFilter!]!
|
|
$selectedHistograms: [String!]
|
|
$numDurationBins: String
|
|
) {
|
|
jobsStatistics(
|
|
filter: $filter
|
|
metrics: $selectedHistograms
|
|
numDurationBins: $numDurationBins
|
|
) {
|
|
histDuration {
|
|
count
|
|
value
|
|
}
|
|
histNumNodes {
|
|
count
|
|
value
|
|
}
|
|
histNumAccs {
|
|
count
|
|
value
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
variables: {
|
|
filter: statusFilter,
|
|
selectedHistograms: selectedHistograms, // No Metrics requested for node hardware stats
|
|
numDurationBins: numDurationBins,
|
|
},
|
|
requestPolicy: "network-only",
|
|
})
|
|
: null,
|
|
);
|
|
|
|
/* Functions */
|
|
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>
|
|
|
|
<!-- 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={() => {
|
|
pagingState = { page: 1, itemsPerPage: 10 };
|
|
selectedHistograms = [...$state.snapshot(selectedHistograms)];
|
|
}}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
<hr />
|
|
|
|
<!-- Job Duration, Top Users and Projects-->
|
|
{#if $topStatsQuery?.fetching || $nodeStatusQuery?.fetching}
|
|
<Spinner />
|
|
{:else if $topStatsQuery?.data && $nodeStatusQuery?.data}
|
|
<Row>
|
|
<Col xs="12" lg="4" class="p-2">
|
|
{#key $nodeStatusQuery?.data?.jobsStatistics[0]?.histDuration}
|
|
<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
|
|
enableFlip
|
|
/>
|
|
{/key}
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<div bind:clientWidth={colWidthJobs}>
|
|
<h4 class="text-center">Top Users: Jobs</h4>
|
|
<Pie
|
|
{useAltColors}
|
|
canvasId="{canvasPrefix}-hpcpie-jobs-users"
|
|
size={colWidthJobs * 0.75}
|
|
sliceLabel="Jobs"
|
|
quantities={topUserJobs.map(
|
|
(tu) => tu["totalJobs"],
|
|
)}
|
|
entities={topUserJobs.map((tu) =>
|
|
scrambleNames ? scramble(tu.id) : tu.id,
|
|
)}
|
|
/>
|
|
</div>
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<Table>
|
|
<tr class="mb-2">
|
|
<th></th>
|
|
<th style="padding-left: 0.5rem;">User</th>
|
|
<th>Jobs</th>
|
|
</tr>
|
|
{#each topUserJobs as tu, i}
|
|
<tr>
|
|
<td
|
|
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
|
|
>
|
|
<td id="{canvasPrefix}-topName-jobs-{tu.id}">
|
|
<a
|
|
target="_blank"
|
|
href="/monitoring/user/{tu.id}?cluster={presetCluster}{presetSubCluster
|
|
? '&partition=' + presetSubCluster
|
|
: ''}&state=running"
|
|
>{scrambleNames ? scramble(tu.id) : tu.id}
|
|
</a>
|
|
</td>
|
|
{#if tu?.name}
|
|
<Tooltip
|
|
target={`${canvasPrefix}-topName-jobs-${tu.id}`}
|
|
placement="left"
|
|
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
|
|
>
|
|
{/if}
|
|
<td>{tu["totalJobs"]}</td>
|
|
</tr>
|
|
{/each}
|
|
</Table>
|
|
</Col>
|
|
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<h4 class="text-center">Top Projects: Jobs</h4>
|
|
<Pie
|
|
{useAltColors}
|
|
canvasId="{canvasPrefix}-hpcpie-jobs-projects"
|
|
size={colWidthJobs * 0.75}
|
|
sliceLabel={"Jobs"}
|
|
quantities={topProjectJobs.map(
|
|
(tp) => tp["totalJobs"],
|
|
)}
|
|
entities={topProjectJobs.map((tp) =>
|
|
scrambleNames ? scramble(tp.id) : tp.id,
|
|
)}
|
|
/>
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<Table>
|
|
<tr class="mb-2">
|
|
<th></th>
|
|
<th style="padding-left: 0.5rem;">Project</th>
|
|
<th>Jobs</th>
|
|
</tr>
|
|
{#each topProjectJobs as tp, i}
|
|
<tr>
|
|
<td
|
|
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
|
|
>
|
|
<td>
|
|
<a
|
|
target="_blank"
|
|
href="/monitoring/jobs/?cluster={presetCluster}{presetSubCluster
|
|
? '&partition=' + presetSubCluster
|
|
: ''}&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 $topStatsQuery?.fetching || $nodeStatusQuery?.fetching}
|
|
<Spinner />
|
|
{:else if $topStatsQuery?.data && $nodeStatusQuery?.data}
|
|
<Row>
|
|
<Col xs="12" lg="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"
|
|
enableFlip
|
|
/>
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<div bind:clientWidth={colWidthNodes}>
|
|
<h4 class="text-center">Top Users: Nodes</h4>
|
|
<Pie
|
|
{useAltColors}
|
|
canvasId="{canvasPrefix}-hpcpie-nodes-users"
|
|
size={colWidthNodes * 0.75}
|
|
sliceLabel="Nodes"
|
|
quantities={topUserNodes.map(
|
|
(tu) => tu["totalNodes"],
|
|
)}
|
|
entities={topUserNodes.map((tu) =>
|
|
scrambleNames ? scramble(tu.id) : tu.id,
|
|
)}
|
|
/>
|
|
</div>
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<Table>
|
|
<tr class="mb-2">
|
|
<th></th>
|
|
<th style="padding-left: 0.5rem;">User</th>
|
|
<th>Nodes</th>
|
|
</tr>
|
|
{#each topUserNodes as tu, i}
|
|
<tr>
|
|
<td
|
|
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
|
|
>
|
|
<td id="{canvasPrefix}-topName-nodes-{tu.id}">
|
|
<a
|
|
target="_blank"
|
|
href="/monitoring/user/{tu.id}?cluster={presetCluster}{presetSubCluster
|
|
? '&partition=' + presetSubCluster
|
|
: ''}&state=running"
|
|
>{scrambleNames ? scramble(tu.id) : tu.id}
|
|
</a>
|
|
</td>
|
|
{#if tu?.name}
|
|
<Tooltip
|
|
target={`${canvasPrefix}-topName-nodes-${tu.id}`}
|
|
placement="left"
|
|
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
|
|
>
|
|
{/if}
|
|
<td>{tu["totalNodes"]}</td>
|
|
</tr>
|
|
{/each}
|
|
</Table>
|
|
</Col>
|
|
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<h4 class="text-center">Top Projects: Nodes</h4>
|
|
<Pie
|
|
{useAltColors}
|
|
canvasId="{canvasPrefix}-hpcpie-nodes-projects"
|
|
size={colWidthNodes * 0.75}
|
|
sliceLabel={"Nodes"}
|
|
quantities={topProjectNodes.map(
|
|
(tp) => tp["totalNodes"],
|
|
)}
|
|
entities={topProjectNodes.map((tp) =>
|
|
scrambleNames ? scramble(tp.id) : tp.id,
|
|
)}
|
|
/>
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<Table>
|
|
<tr class="mb-2">
|
|
<th></th>
|
|
<th style="padding-left: 0.5rem;">Project</th>
|
|
<th>Nodes</th>
|
|
</tr>
|
|
{#each topProjectNodes as tp, i}
|
|
<tr>
|
|
<td
|
|
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
|
|
>
|
|
<td>
|
|
<a
|
|
target="_blank"
|
|
href="/monitoring/jobs/?cluster={presetCluster}{presetSubCluster
|
|
? '&partition=' + presetSubCluster
|
|
: ''}&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 $topStatsQuery?.fetching || $nodeStatusQuery?.fetching}
|
|
<Spinner />
|
|
{:else if $topStatsQuery?.data && $nodeStatusQuery?.data}
|
|
<Row>
|
|
<Col xs="12" lg="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"
|
|
enableFlip
|
|
/>
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<div bind:clientWidth={colWidthAccs}>
|
|
<h4 class="text-center">Top Users: GPUs</h4>
|
|
<Pie
|
|
{useAltColors}
|
|
canvasId="{canvasPrefix}-hpcpie-accs-users"
|
|
size={colWidthAccs * 0.75}
|
|
sliceLabel="GPUs"
|
|
quantities={topUserAccs.map(
|
|
(tu) => tu["totalAccs"],
|
|
)}
|
|
entities={topUserAccs.map((tu) =>
|
|
scrambleNames ? scramble(tu.id) : tu.id,
|
|
)}
|
|
/>
|
|
</div>
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<Table>
|
|
<tr class="mb-2">
|
|
<th></th>
|
|
<th style="padding-left: 0.5rem;">User</th>
|
|
<th>GPUs</th>
|
|
</tr>
|
|
{#each topUserAccs as tu, i}
|
|
<tr>
|
|
<td
|
|
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
|
|
>
|
|
<td id="{canvasPrefix}-topName-accs-{tu.id}">
|
|
<a
|
|
target="_blank"
|
|
href="/monitoring/user/{tu.id}?cluster={presetCluster}{presetSubCluster
|
|
? '&partition=' + presetSubCluster
|
|
: ''}&state=running"
|
|
>{scrambleNames ? scramble(tu.id) : tu.id}
|
|
</a>
|
|
</td>
|
|
{#if tu?.name}
|
|
<Tooltip
|
|
target={`${canvasPrefix}-topName-accs-${tu.id}`}
|
|
placement="left"
|
|
>{scrambleNames ? scramble(tu.name) : tu.name}</Tooltip
|
|
>
|
|
{/if}
|
|
<td>{tu["totalAccs"]}</td>
|
|
</tr>
|
|
{/each}
|
|
</Table>
|
|
</Col>
|
|
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<h4 class="text-center">Top Projects: GPUs</h4>
|
|
<Pie
|
|
{useAltColors}
|
|
canvasId="{canvasPrefix}-hpcpie-accs-projects"
|
|
size={colWidthAccs * 0.75}
|
|
sliceLabel={"GPUs"}
|
|
quantities={topProjectAccs.map(
|
|
(tp) => tp["totalAccs"],
|
|
)}
|
|
entities={topProjectAccs.map((tp) =>
|
|
scrambleNames ? scramble(tp.id) : tp.id,
|
|
)}
|
|
/>
|
|
</Col>
|
|
<Col xs="6" md="3" lg="2" class="p-2">
|
|
<Table>
|
|
<tr class="mb-2">
|
|
<th></th>
|
|
<th style="padding-left: 0.5rem;">Project</th>
|
|
<th>GPUs</th>
|
|
</tr>
|
|
{#each topProjectAccs as tp, i}
|
|
<tr>
|
|
<td
|
|
><Icon name="circle-fill" style="color: {legendColors(i)};" /></td
|
|
>
|
|
<td>
|
|
<a
|
|
target="_blank"
|
|
href="/monitoring/jobs/?cluster={presetCluster}{presetSubCluster
|
|
? '&partition=' + presetSubCluster
|
|
: ''}&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}
|