mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-26 14:16:07 +02:00
add extended legend for nodelist acc metrics, move nodelist paging select
This commit is contained in:
@@ -9,7 +9,6 @@
|
||||
|
||||
<script>
|
||||
import {
|
||||
Spinner,
|
||||
Icon,
|
||||
Button,
|
||||
Card,
|
||||
@@ -18,61 +17,24 @@
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText, } from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient,
|
||||
} from "@urql/svelte";
|
||||
|
||||
export let cluster;
|
||||
export let subCluster
|
||||
export let hostname;
|
||||
export let dataHealth;
|
||||
|
||||
const client = getContextClient();
|
||||
const paging = { itemsPerPage: 50, page: 1 };
|
||||
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||
const filter = [
|
||||
{ cluster: { eq: cluster } },
|
||||
{ node: { contains: hostname } },
|
||||
{ state: ["running"] },
|
||||
];
|
||||
|
||||
const nodeJobsQuery = gql`
|
||||
query (
|
||||
$filter: [JobFilter!]!
|
||||
$sorting: OrderByInput!
|
||||
$paging: PageRequest!
|
||||
) {
|
||||
jobs(filter: $filter, order: $sorting, page: $paging) {
|
||||
items {
|
||||
user
|
||||
project
|
||||
exclusive
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
export let nodeJobsData = null;
|
||||
|
||||
// Not at least one returned, selected metric: NodeHealth warning
|
||||
const healthWarn = !dataHealth.includes(true);
|
||||
// At least one non-returned selected metric: Metric config error?
|
||||
const metricWarn = dataHealth.includes(false);
|
||||
|
||||
$: nodeJobsData = queryStore({
|
||||
client: client,
|
||||
query: nodeJobsQuery,
|
||||
variables: { paging, sorting, filter },
|
||||
});
|
||||
|
||||
let userList;
|
||||
let projectList;
|
||||
$: if ($nodeJobsData?.data) {
|
||||
userList = Array.from(new Set($nodeJobsData.data.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b));
|
||||
projectList = Array.from(new Set($nodeJobsData.data.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
$: if (nodeJobsData) {
|
||||
userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b));
|
||||
projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="pb-3">
|
||||
@@ -92,127 +54,123 @@
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#if $nodeJobsData.fetching}
|
||||
<Spinner />
|
||||
{:else if $nodeJobsData.data}
|
||||
{#if healthWarn}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="exclamation-circle"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="danger" disabled>
|
||||
Unhealthy
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{:else if metricWarn}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="info-circle"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="warning" disabled>
|
||||
Missing Metric
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{:else if $nodeJobsData.data.jobs.count == 1 && $nodeJobsData.data.jobs.items[0].exclusive}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle-fill"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="success" disabled>
|
||||
Exclusive
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{:else if $nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle-half"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="success" disabled>
|
||||
Shared
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{:else}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="secondary" disabled>
|
||||
Idle
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{/if}
|
||||
<hr class="my-3"/>
|
||||
<!-- JOBS -->
|
||||
<InputGroup size="sm" class="justify-content-between mb-3">
|
||||
{#if healthWarn}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="activity"/>
|
||||
<Icon name="exclamation-circle"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
|
||||
Activity
|
||||
</InputGroupText>
|
||||
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{$nodeJobsData?.data?.jobs?.count || 0} Job{($nodeJobsData?.data?.jobs?.count == 1) ? '': 's'}" disabled />
|
||||
<a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
|
||||
<Icon name="view-list" />
|
||||
List
|
||||
</a>
|
||||
</InputGroup>
|
||||
<!-- USERS -->
|
||||
<InputGroup size="sm" class="justify-content-between {(userList?.length > 0) ? 'mb-1' : 'mb-3'}">
|
||||
<InputGroupText>
|
||||
<Icon name="people"/>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
|
||||
Users
|
||||
</InputGroupText>
|
||||
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{userList?.length || 0} User{(userList?.length == 1) ? '': 's'}" disabled />
|
||||
<a title="Show users active on this node" href="/monitoring/users/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
|
||||
<Icon name="view-list" />
|
||||
List
|
||||
</a>
|
||||
<Button color="danger" disabled>
|
||||
Unhealthy
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{#if userList?.length > 0}
|
||||
<Card class="mb-3">
|
||||
<div class="p-1">
|
||||
{userList.join(", ")}
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
<!-- PROJECTS -->
|
||||
<InputGroup size="sm" class="justify-content-between {(projectList?.length > 0) ? 'mb-1' : 'mb-3'}">
|
||||
{:else if metricWarn}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="journals"/>
|
||||
<Icon name="info-circle"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
|
||||
Projects
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{projectList?.length || 0} Project{(projectList?.length == 1) ? '': 's'}" disabled />
|
||||
<a title="Show projects active on this node" href="/monitoring/projects/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
|
||||
<Icon name="view-list" />
|
||||
List
|
||||
</a>
|
||||
<Button color="warning" disabled>
|
||||
Missing Metric
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{#if projectList?.length > 0}
|
||||
<Card>
|
||||
<div class="p-1">
|
||||
{projectList.join(", ")}
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
{:else if nodeJobsData.jobs.count == 1 && nodeJobsData.jobs.items[0].exclusive}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle-fill"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="success" disabled>
|
||||
Exclusive
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{:else if nodeJobsData.jobs.count >= 1 && !nodeJobsData.jobs.items[0].exclusive}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle-half"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="success" disabled>
|
||||
Shared
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{:else}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="secondary" disabled>
|
||||
Idle
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{/if}
|
||||
<hr class="my-3"/>
|
||||
<!-- JOBS -->
|
||||
<InputGroup size="sm" class="justify-content-between mb-3">
|
||||
<InputGroupText>
|
||||
<Icon name="activity"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
|
||||
Activity
|
||||
</InputGroupText>
|
||||
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{nodeJobsData?.jobs?.count || 0} Job{(nodeJobsData?.jobs?.count == 1) ? '': 's'}" disabled />
|
||||
<a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
|
||||
<Icon name="view-list" />
|
||||
List
|
||||
</a>
|
||||
</InputGroup>
|
||||
<!-- USERS -->
|
||||
<InputGroup size="sm" class="justify-content-between {(userList?.length > 0) ? 'mb-1' : 'mb-3'}">
|
||||
<InputGroupText>
|
||||
<Icon name="people"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
|
||||
Users
|
||||
</InputGroupText>
|
||||
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{userList?.length || 0} User{(userList?.length == 1) ? '': 's'}" disabled />
|
||||
<a title="Show users active on this node" href="/monitoring/users/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
|
||||
<Icon name="view-list" />
|
||||
List
|
||||
</a>
|
||||
</InputGroup>
|
||||
{#if userList?.length > 0}
|
||||
<Card class="mb-3">
|
||||
<div class="p-1">
|
||||
{userList.join(", ")}
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
<!-- PROJECTS -->
|
||||
<InputGroup size="sm" class="justify-content-between {(projectList?.length > 0) ? 'mb-1' : 'mb-3'}">
|
||||
<InputGroupText>
|
||||
<Icon name="journals"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
|
||||
Projects
|
||||
</InputGroupText>
|
||||
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{projectList?.length || 0} Project{(projectList?.length == 1) ? '': 's'}" disabled />
|
||||
<a title="Show projects active on this node" href="/monitoring/projects/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-primary" role="button" aria-disabled="true" >
|
||||
<Icon name="view-list" />
|
||||
List
|
||||
</a>
|
||||
</InputGroup>
|
||||
{#if projectList?.length > 0}
|
||||
<Card>
|
||||
<div class="p-1">
|
||||
{projectList.join(", ")}
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
@@ -8,7 +8,12 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Card } from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient,
|
||||
} from "@urql/svelte";
|
||||
import { Card, CardBody, Spinner } from "@sveltestrap/sveltestrap";
|
||||
import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
|
||||
import MetricPlot from "../../generic/plots/MetricPlot.svelte";
|
||||
import NodeInfo from "./NodeInfo.svelte";
|
||||
@@ -17,6 +22,43 @@
|
||||
export let nodeData;
|
||||
export let selectedMetrics;
|
||||
|
||||
const client = getContextClient();
|
||||
const paging = { itemsPerPage: 50, page: 1 };
|
||||
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||
const filter = [
|
||||
{ cluster: { eq: cluster } },
|
||||
{ node: { contains: nodeData.host } },
|
||||
{ state: ["running"] },
|
||||
];
|
||||
|
||||
const nodeJobsQuery = gql`
|
||||
query (
|
||||
$filter: [JobFilter!]!
|
||||
$sorting: OrderByInput!
|
||||
$paging: PageRequest!
|
||||
) {
|
||||
jobs(filter: $filter, order: $sorting, page: $paging) {
|
||||
items {
|
||||
jobId
|
||||
user
|
||||
project
|
||||
exclusive
|
||||
resources {
|
||||
hostname
|
||||
accelerators
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
$: nodeJobsData = queryStore({
|
||||
client: client,
|
||||
query: nodeJobsQuery,
|
||||
variables: { paging, sorting, filter },
|
||||
});
|
||||
|
||||
// Helper
|
||||
const selectScope = (nodeMetrics) =>
|
||||
nodeMetrics.reduce(
|
||||
@@ -51,14 +93,44 @@
|
||||
let dataHealth;
|
||||
$: if (nodeData?.metrics) {
|
||||
refinedData = sortAndSelectScope(nodeData?.metrics)
|
||||
// Check data for series, skip disabled
|
||||
dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0))
|
||||
}
|
||||
|
||||
let extendedLegendData = null;
|
||||
$: if ($nodeJobsData?.data) {
|
||||
// Get Shared State of Node: Only Build extended Legend For Shared Nodes
|
||||
if ($nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive) {
|
||||
const accSet = Array.from(new Set($nodeJobsData.data.jobs.items
|
||||
.map((i) => i.resources
|
||||
.filter((r) => r.hostname === nodeData.host)
|
||||
.map((r) => r.accelerators)
|
||||
)
|
||||
)).flat(2)
|
||||
|
||||
extendedLegendData = {}
|
||||
for (const accId of accSet) {
|
||||
const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId)))
|
||||
extendedLegendData[accId] = {
|
||||
user: matchJob?.user ? matchJob?.user : '-',
|
||||
job: matchJob?.jobId ? matchJob?.jobId : '-',
|
||||
}
|
||||
}
|
||||
// Theoretically extendable for hwthreadIDs
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} {dataHealth}/>
|
||||
{#if $nodeJobsData.fetching}
|
||||
<Card>
|
||||
<CardBody class="content-center">
|
||||
<Spinner/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:else}
|
||||
<NodeInfo nodeJobsData={$nodeJobsData.data} {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} {dataHealth}/>
|
||||
{/if}
|
||||
</td>
|
||||
{#each refinedData as metricData (metricData.data.name)}
|
||||
<td>
|
||||
@@ -83,16 +155,19 @@
|
||||
forNode
|
||||
/>
|
||||
<div class="my-2"/>
|
||||
<MetricPlot
|
||||
{cluster}
|
||||
subCluster={nodeData.subCluster}
|
||||
metric={metricData.data.name}
|
||||
scope={metricData.data.scope}
|
||||
timestep={metricData.data.metric.timestep}
|
||||
series={metricData.data.metric.series}
|
||||
height={175}
|
||||
forNode
|
||||
/>
|
||||
{#key extendedLegendData}
|
||||
<MetricPlot
|
||||
{cluster}
|
||||
subCluster={nodeData.subCluster}
|
||||
metric={metricData.data.name}
|
||||
scope={metricData.data.scope}
|
||||
timestep={metricData.data.metric.timestep}
|
||||
series={metricData.data.metric.series}
|
||||
height={175}
|
||||
{extendedLegendData}
|
||||
forNode
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
<MetricPlot
|
||||
{cluster}
|
||||
|
Reference in New Issue
Block a user