mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-23 12:51:40 +02:00
fix legends, add resolution, add statsseries, add simple healthcheck
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import { getContext, createEventDispatcher } from "svelte";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
@@ -52,15 +52,22 @@
|
||||
const globalMetrics = getContext("globalMetrics");
|
||||
const displayNodeOverview = (displayType === 'OVERVIEW')
|
||||
|
||||
const resampleConfig = getContext("resampling") || null;
|
||||
const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
|
||||
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
||||
let selectedResolution = resampleConfig ? resampleDefault : 0;
|
||||
|
||||
let hostnameFilter = "";
|
||||
let pendingHostnameFilter = "";
|
||||
let selectedMetric = ccconfig.system_view_selectedMetric || "";
|
||||
let selectedMetrics = ccconfig[`node_list_selectedMetrics:${cluster}`] || [ccconfig.system_view_selectedMetric];
|
||||
let isMetricsSelectionOpen = false;
|
||||
|
||||
/*
|
||||
Note 1: Scope Selector or Auto-Scoped?
|
||||
Note 2: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster
|
||||
Note 1: Scope Selector or Auto-Scoped? -> USeful auto scoping with stats view where applicable -> CHeck with JVe
|
||||
Note 2: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
|
||||
Note 3: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
|
||||
Note 4: Resolution changes as implemented only possible for all plots generally, not for individual metrics: Result list if build from GQL result *including* metric series
|
||||
*/
|
||||
|
||||
let systemMetrics = [];
|
||||
@@ -80,10 +87,15 @@
|
||||
selectedMetrics = [selectedMetric]
|
||||
}
|
||||
|
||||
$: { // Wait after input for some time to prevent too many requests
|
||||
setTimeout(function () {
|
||||
hostnameFilter = pendingHostnameFilter;
|
||||
}, 500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ROW1: Tools-->
|
||||
<Row cols={{ xs: 2, lg: 4 }} class="mb-3">
|
||||
<Row cols={{ xs: 2, lg: !displayNodeOverview ? 5 : 4 }} class="mb-3">
|
||||
{#if $initq.data}
|
||||
<!-- List Metric Select Col-->
|
||||
{#if !displayNodeOverview}
|
||||
@@ -91,7 +103,7 @@
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
||||
<InputGroupText class="text-capitalize">Metrics</InputGroupText>
|
||||
<Button
|
||||
<Button
|
||||
outline
|
||||
color="primary"
|
||||
on:click={() => (isMetricsSelectionOpen = true)}
|
||||
@@ -99,17 +111,30 @@
|
||||
{selectedMetrics.length} selected
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Col>
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>
|
||||
<InputGroupText>Resolution</InputGroupText>
|
||||
<Input type="select" bind:value={selectedResolution}>
|
||||
{#each resampleResolutions as res}
|
||||
<option value={res}
|
||||
>{res} sec</option
|
||||
>
|
||||
{/each}
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
{/if}
|
||||
<!-- Node Col-->
|
||||
<Col>
|
||||
<Col class="mt-2 mt-lg-0">
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
||||
<InputGroupText>Find Node(s)</InputGroupText>
|
||||
<Input
|
||||
placeholder="Filter hostname ..."
|
||||
type="text"
|
||||
bind:value={hostnameFilter}
|
||||
bind:value={pendingHostnameFilter}
|
||||
/>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
@@ -159,7 +184,7 @@
|
||||
<NodeOverview {cluster} {subCluster} {ccconfig} {selectedMetrics} {from} {to} {hostnameFilter}/>
|
||||
{:else}
|
||||
<!-- ROW2-2: Node List (Grid Included)-->
|
||||
<NodeList {cluster} {subCluster} {ccconfig} {selectedMetrics} {hostnameFilter} {from} {to} {systemUnits}/>
|
||||
<NodeList {cluster} {subCluster} {ccconfig} {selectedMetrics} {selectedResolution} {hostnameFilter} {from} {to} {systemUnits}/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
@@ -142,7 +142,7 @@
|
||||
if (useStatsSeries == null) useStatsSeries = statisticsSeries != null;
|
||||
if (useStatsSeries == false && series == null) useStatsSeries = true;
|
||||
|
||||
const usesMeanStatsSeries = (useStatsSeries && statisticsSeries.mean.length != 0)
|
||||
const usesMeanStatsSeries = (useStatsSeries?.mean && statisticsSeries.mean.length != 0)
|
||||
const dispatch = createEventDispatcher();
|
||||
const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
|
||||
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
|
||||
|
@@ -11,6 +11,7 @@ new Systems({
|
||||
to: infos.to
|
||||
},
|
||||
context: new Map([
|
||||
['cc-config', clusterCockpitConfig]
|
||||
['cc-config', clusterCockpitConfig],
|
||||
['resampling', resampleConfig]
|
||||
])
|
||||
})
|
||||
|
@@ -20,6 +20,7 @@
|
||||
export let subCluster = "";
|
||||
export const ccconfig = null;
|
||||
export let selectedMetrics = [];
|
||||
export let selectedResolution = 0;
|
||||
export let hostnameFilter = "";
|
||||
export let systemUnits = null;
|
||||
export let from = null;
|
||||
@@ -39,7 +40,7 @@
|
||||
const { query: initq } = init();
|
||||
const client = getContextClient();
|
||||
const nodeListQuery = gql`
|
||||
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!) {
|
||||
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
|
||||
nodeMetricsList(
|
||||
cluster: $cluster
|
||||
subCluster: $subCluster
|
||||
@@ -49,6 +50,7 @@
|
||||
from: $from
|
||||
to: $to
|
||||
page: $paging
|
||||
resolution: $selectedResolution
|
||||
) {
|
||||
items {
|
||||
host
|
||||
@@ -63,12 +65,19 @@
|
||||
prefix
|
||||
}
|
||||
series {
|
||||
id
|
||||
hostname
|
||||
data
|
||||
statistics {
|
||||
min
|
||||
avg
|
||||
max
|
||||
}
|
||||
data
|
||||
}
|
||||
statisticsSeries {
|
||||
min
|
||||
median
|
||||
max
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,15 +95,19 @@
|
||||
cluster: cluster,
|
||||
subCluster: subCluster,
|
||||
nodeFilter: hostnameFilter,
|
||||
scopes: ["core", "accelerator"],
|
||||
scopes: ["core", "socket", "accelerator"],
|
||||
metrics: selectedMetrics,
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
paging: paging,
|
||||
selectedResolution: selectedResolution,
|
||||
},
|
||||
requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change
|
||||
});
|
||||
|
||||
$: matchedNodes = $nodesQuery.data?.nodeMetricsList.totalNodes || 0;
|
||||
$: orderedData = $nodesQuery.data?.nodeMetricsList.items.sort((a, b) => a.host.localeCompare(b.host));
|
||||
|
||||
</script>
|
||||
|
||||
{#if $nodesQuery.error}
|
||||
@@ -135,7 +148,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $nodesQuery.data.nodeMetricsList.items as nodeData (nodeData.host)}
|
||||
{#each orderedData as nodeData (nodeData.host)}
|
||||
<NodeListRow {nodeData} {cluster} {selectedMetrics}/>
|
||||
{:else}
|
||||
<tr>
|
||||
|
@@ -27,6 +27,7 @@
|
||||
export let cluster;
|
||||
export let subCluster
|
||||
export let hostname;
|
||||
export let dataHealth;
|
||||
|
||||
const client = getContextClient();
|
||||
const paging = { itemsPerPage: 50, page: 1 };
|
||||
@@ -49,6 +50,11 @@
|
||||
}
|
||||
`;
|
||||
|
||||
// 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,
|
||||
@@ -78,7 +84,31 @@
|
||||
<Spinner />
|
||||
{:else if $nodeJobsData.data}
|
||||
<p>
|
||||
{#if $nodeJobsData.data.jobs.count > 0}
|
||||
{#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="circle-half"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText>
|
||||
Status
|
||||
</InputGroupText>
|
||||
<Button color="warning" disabled>
|
||||
Missing Metric
|
||||
</Button>
|
||||
</InputGroup>
|
||||
{:else if $nodeJobsData.data.jobs.count > 0}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="circle-fill"/>
|
||||
|
@@ -50,7 +50,7 @@
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} />
|
||||
<NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} dataHealth={nodeData?.metrics.map((m) => (m.metric.series.length > 0))}/>
|
||||
</td>
|
||||
{#each sortAndSelectScope(nodeData?.metrics) as metricData (metricData.data.name)}
|
||||
<td>
|
||||
@@ -63,11 +63,14 @@
|
||||
{:else}
|
||||
<!-- "No Data"-Warning included in MetricPlot-Component -->
|
||||
<MetricPlot
|
||||
timestep={metricData.data.metric.timestep}
|
||||
series={metricData.data.metric.series}
|
||||
metric={metricData.data.name}
|
||||
{cluster}
|
||||
subCluster={nodeData.subCluster}
|
||||
metric={metricData.data.name}
|
||||
scope={metricData.data.scope}
|
||||
timestep={metricData.data.metric.timestep}
|
||||
series={metricData.data.metric.series}
|
||||
statisticsSeries={metricData.data?.metric.statisticsSeries}
|
||||
useStatsSeries={!!metricData.data?.metric.statisticsSeries}
|
||||
forNode
|
||||
/>
|
||||
{/if}
|
||||
|
Reference in New Issue
Block a user