mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-11-26 11:33:07 +01:00
add nodeState info display and filtering to systems views
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
- `selectedMetrics [String]`: The array of selected metrics [Default []]
|
||||
- `selectedResolution Number?`: The selected data resolution [Default: 0]
|
||||
- `hostnameFilter String?`: The active hostnamefilter [Default: ""]
|
||||
- `hoststateFilter String?`: The active hoststatefilter [Default: ""]
|
||||
- `presetSystemUnits Object`: The object of metric units [Default: null]
|
||||
- `from Date?`: The selected "from" date [Default: null]
|
||||
- `to Date?`: The selected "to" date [Default: null]
|
||||
@@ -28,6 +29,7 @@
|
||||
selectedMetrics = [],
|
||||
selectedResolution = 0,
|
||||
hostnameFilter = "",
|
||||
hoststateFilter = "",
|
||||
presetSystemUnits = null,
|
||||
from = null,
|
||||
to = null
|
||||
@@ -37,11 +39,14 @@
|
||||
const client = getContextClient();
|
||||
const usePaging = ccconfig?.nodeList_usePaging || false;
|
||||
const nodeListQuery = gql`
|
||||
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
|
||||
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $stateFilter: String!, $metrics: [String!],
|
||||
$scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int
|
||||
) {
|
||||
nodeMetricsList(
|
||||
cluster: $cluster
|
||||
subCluster: $subCluster
|
||||
nodeFilter: $nodeFilter
|
||||
stateFilter: $stateFilter,
|
||||
scopes: $scopes
|
||||
metrics: $metrics
|
||||
from: $from
|
||||
@@ -51,6 +56,7 @@
|
||||
) {
|
||||
items {
|
||||
host
|
||||
state
|
||||
subCluster
|
||||
metrics {
|
||||
name
|
||||
@@ -100,6 +106,7 @@
|
||||
variables: {
|
||||
cluster: cluster,
|
||||
subCluster: subCluster,
|
||||
stateFilter: hoststateFilter,
|
||||
nodeFilter: hostnameFilter,
|
||||
scopes: ["core", "socket", "accelerator"],
|
||||
metrics: selectedMetrics,
|
||||
@@ -137,7 +144,7 @@
|
||||
// Triggers (Except Paging)
|
||||
from, to
|
||||
selectedMetrics, selectedResolution
|
||||
hostnameFilter
|
||||
hostnameFilter, hoststateFilter
|
||||
// Continous Scroll: Reset nodes and paging if parameters change: Existing entries will not match new selections
|
||||
if (!usePaging) {
|
||||
nodes = [];
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- `cluster String`: The cluster to show status information for
|
||||
- `selectedMetric String?`: The selectedMetric input [Default: ""]
|
||||
- `hostnameFilter String?`: The active hostnamefilter [Default: ""]
|
||||
- `hostnameFilter String?`: The active hoststatefilter [Default: ""]
|
||||
- `from Date?`: The selected "from" date [Default: null]
|
||||
- `to Date?`: The selected "to" date [Default: null]
|
||||
-->
|
||||
@@ -13,7 +14,7 @@
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import { Row, Col, Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||
import { Row, Col, Card, Spinner, Badge } from "@sveltestrap/sveltestrap";
|
||||
import { checkMetricDisabled } from "../generic/utils.js";
|
||||
import MetricPlot from "../generic/plots/MetricPlot.svelte";
|
||||
|
||||
@@ -23,6 +24,7 @@
|
||||
cluster = "",
|
||||
selectedMetric = "",
|
||||
hostnameFilter = "",
|
||||
hoststateFilter = "",
|
||||
from = null,
|
||||
to = null
|
||||
} = $props();
|
||||
@@ -30,6 +32,16 @@
|
||||
/* Const Init */
|
||||
const initialized = getContext("initialized");
|
||||
const client = getContextClient();
|
||||
// Node State Colors
|
||||
const stateColors = {
|
||||
allocated: 'success',
|
||||
reserved: 'info',
|
||||
idle: 'primary',
|
||||
mixed: 'warning',
|
||||
down: 'danger',
|
||||
unknown: 'dark',
|
||||
notindb: 'secondary'
|
||||
}
|
||||
|
||||
/* Derived */
|
||||
const nodesQuery = $derived(queryStore({
|
||||
@@ -43,6 +55,7 @@
|
||||
to: $to
|
||||
) {
|
||||
host
|
||||
state
|
||||
subCluster
|
||||
metrics {
|
||||
name
|
||||
@@ -75,7 +88,15 @@
|
||||
}));
|
||||
|
||||
const mappedData = $derived(handleQueryData($initialized, $nodesQuery?.data));
|
||||
const filteredData = $derived(mappedData.filter((h) => h.host.includes(hostnameFilter)));
|
||||
const filteredData = $derived(mappedData.filter((h) => {
|
||||
if (hostnameFilter) {
|
||||
if (hoststateFilter == 'all') return h.host.includes(hostnameFilter)
|
||||
else return (h.host.includes(hostnameFilter) && h.state == hoststateFilter)
|
||||
} else {
|
||||
if (hoststateFilter == 'all') return true
|
||||
else return h.state == hoststateFilter
|
||||
}
|
||||
}));
|
||||
|
||||
/* Functions */
|
||||
function handleQueryData(isInitialized, queryData) {
|
||||
@@ -94,6 +115,7 @@
|
||||
if (rawData.length > 0) {
|
||||
pendingMapped = rawData.map((h) => ({
|
||||
host: h.host,
|
||||
state: h?.state? h.state : 'notindb',
|
||||
subCluster: h.subCluster,
|
||||
data: h.metrics.filter(
|
||||
(m) => m?.name == selectedMetric && m.scope == "node",
|
||||
@@ -125,13 +147,18 @@
|
||||
{#key selectedMetric}
|
||||
{#each filteredData as item (item.host)}
|
||||
<Col class="px-1">
|
||||
<h4 style="width: 100%; text-align: center;">
|
||||
<a
|
||||
style="display: block;padding-top: 15px;"
|
||||
href="/monitoring/node/{cluster}/{item.host}"
|
||||
>{item.host} ({item.subCluster})</a
|
||||
>
|
||||
</h4>
|
||||
<div class="d-flex align-items-baseline">
|
||||
<h4 style="width: 100%; text-align: center;">
|
||||
<a
|
||||
style="display: block;padding-top: 15px;"
|
||||
href="/monitoring/node/{cluster}/{item.host}"
|
||||
>{item.host} ({item.subCluster})</a
|
||||
>
|
||||
</h4>
|
||||
<span style="margin-right: 0.5rem;">
|
||||
<Badge color={stateColors[item?.state? item.state : 'notindb']}>{item?.state? item.state : 'notindb'}</Badge>
|
||||
</span>
|
||||
</div>
|
||||
{#if item.disabled === true}
|
||||
<Card body class="mx-3" color="info"
|
||||
>Metric disabled for subcluster <code
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
import {
|
||||
Icon,
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
@@ -30,6 +32,7 @@
|
||||
cluster,
|
||||
subCluster,
|
||||
hostname,
|
||||
hoststate,
|
||||
dataHealth,
|
||||
nodeJobsData = null,
|
||||
} = $props();
|
||||
@@ -39,6 +42,16 @@
|
||||
const healthWarn = !dataHealth.includes(true);
|
||||
// At least one non-returned selected metric: Metric config error?
|
||||
const metricWarn = dataHealth.includes(false);
|
||||
// Node State Colors
|
||||
const stateColors = {
|
||||
allocated: 'success',
|
||||
reserved: 'info',
|
||||
idle: 'primary',
|
||||
mixed: 'warning',
|
||||
down: 'danger',
|
||||
unknown: 'dark',
|
||||
notindb: 'secondary'
|
||||
}
|
||||
|
||||
/* Derived */
|
||||
const userList = $derived(nodeJobsData
|
||||
@@ -68,80 +81,72 @@
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{#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.jobs.count == 1 && nodeJobsData.jobs.items[0].shared == "none"}
|
||||
<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].shared == "none")}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle-half"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="success" disabled>
|
||||
Shared
|
||||
</Button>
|
||||
</InputGroup>
|
||||
<!-- Fallback -->
|
||||
{:else if nodeJobsData.jobs.count >= 1}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle-fill"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="success" disabled>
|
||||
Allocated Jobs
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{:else}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="secondary" disabled>
|
||||
Idle
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{/if}
|
||||
<Row cols={{xs: 1, lg: 2}}>
|
||||
<Col class="mb-2 mb-lg-0">
|
||||
<InputGroup size="sm">
|
||||
{#if healthWarn}
|
||||
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
|
||||
<Icon name="exclamation-circle" style="padding-right: 0.5rem;"/>
|
||||
<span>Jobs</span>
|
||||
</InputGroupText>
|
||||
<Button class="flex-grow-1" color="danger" disabled>
|
||||
No Metrics
|
||||
</Button>
|
||||
{:else if metricWarn}
|
||||
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
|
||||
<Icon name="info-circle" style="padding-right: 0.5rem;"/>
|
||||
<span>Jobs</span>
|
||||
</InputGroupText>
|
||||
<Button class="flex-grow-1" color="warning" disabled>
|
||||
Missing Metric
|
||||
</Button>
|
||||
{:else if nodeJobsData.jobs.count == 1 && nodeJobsData.jobs.items[0].shared == "none"}
|
||||
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
|
||||
<Icon name="circle-fill" style="padding-right: 0.5rem;"/>
|
||||
<span>Jobs</span>
|
||||
</InputGroupText>
|
||||
<Button class="flex-grow-1" color="success" disabled>
|
||||
Exclusive
|
||||
</Button>
|
||||
{:else if nodeJobsData.jobs.count >= 1 && !(nodeJobsData.jobs.items[0].shared == "none")}
|
||||
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
|
||||
<Icon name="circle-half" style="padding-right: 0.5rem;"/>
|
||||
<span>Jobs</span>
|
||||
</InputGroupText>
|
||||
<Button class="flex-grow-1" color="success" disabled>
|
||||
Shared
|
||||
</Button>
|
||||
<!-- Fallback -->
|
||||
{:else if nodeJobsData.jobs.count >= 1}
|
||||
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
|
||||
<Icon name="circle-fill" style="padding-right: 0.5rem;"/>
|
||||
<span>Jobs</span>
|
||||
</InputGroupText>
|
||||
<Button class="flex-grow-1" color="success" disabled>
|
||||
Running
|
||||
</Button>
|
||||
{:else}
|
||||
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
|
||||
<Icon name="circle" style="padding-right: 0.5rem;"/>
|
||||
<span>Jobs</span>
|
||||
</InputGroupText>
|
||||
<Button class="flex-grow-1" color="secondary" disabled>
|
||||
None
|
||||
</Button>
|
||||
{/if}
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col>
|
||||
<InputGroup size="sm">
|
||||
<InputGroupText class="flex-grow-1 flex-lg-grow-0">
|
||||
State
|
||||
</InputGroupText>
|
||||
<Button class="flex-grow-1" color={stateColors[hoststate]} disabled>
|
||||
{hoststate.charAt(0).toUpperCase() + hoststate.slice(1)}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr class="my-3"/>
|
||||
<!-- JOBS -->
|
||||
<InputGroup size="sm" class="justify-content-between mb-3">
|
||||
|
||||
@@ -139,7 +139,13 @@
|
||||
</CardBody>
|
||||
</Card>
|
||||
{:else}
|
||||
<NodeInfo nodeJobsData={$nodeJobsData.data} {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} {dataHealth}/>
|
||||
<NodeInfo
|
||||
{cluster}
|
||||
{dataHealth}
|
||||
nodeJobsData={$nodeJobsData.data}
|
||||
subCluster={nodeData.subCluster}
|
||||
hostname={nodeData.host}
|
||||
hoststate={nodeData?.state? nodeData.state: 'notindb'}/>
|
||||
{/if}
|
||||
</td>
|
||||
{#each refinedData as metricData (metricData.data.name)}
|
||||
|
||||
Reference in New Issue
Block a user