migrate system view, node list and node overview

This commit is contained in:
Christoph Kluge
2025-06-12 16:23:31 +02:00
parent a0190f8f40
commit f471214ef7
4 changed files with 290 additions and 257 deletions

View File

@@ -10,38 +10,28 @@
-->
<script>
import { getContext } from "svelte";
import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte";
import { Row, Col, Card, Table, Spinner } from "@sveltestrap/sveltestrap";
import { stickyHeader } from "../generic/utils.js";
import NodeListRow from "./nodelist/NodeListRow.svelte";
import Pagination from "../generic/joblist/Pagination.svelte";
export let cluster;
export let subCluster = "";
export let ccconfig = null;
export let selectedMetrics = [];
export let selectedResolution = 0;
export let hostnameFilter = "";
export let systemUnits = null;
export let from = null;
export let to = null;
/* Svelte 5 Props */
let {
cluster,
subCluster = "",
ccconfig = null,
selectedMetrics = [],
selectedResolution = 0,
hostnameFilter = "",
presetSystemUnits = null,
from = null,
to = null
} = $props();
// Decouple from Job List Paging Params?
let usePaging = ccconfig?.node_list_usePaging || false
let itemsPerPage = usePaging ? (ccconfig?.plot_list_nodesPerPage || 10) : 10;
let page = 1;
let paging = { itemsPerPage, page };
let headerPaddingTop = 0;
stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x),
);
// const { query: initq } = init();
const initialized = getContext("initialized");
/* Const Init */
const client = getContextClient();
const usePaging = ccconfig?.node_list_usePaging || false;
const nodeListQuery = gql`
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
nodeMetricsList(
@@ -91,51 +81,17 @@
}
`
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
/* State Init */
let nodes = $state([]);
let page = $state(1);
let itemsPerPage = $state(usePaging ? (ccconfig?.plot_list_nodesPerPage || 10) : 10);
let headerPaddingTop = $state(0);
// Decouple from Job List Paging Params?
function updateConfiguration(value, page) {
updateConfigurationMutation({
name: "plot_list_nodesPerPage",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
nodes = [] // Empty List
paging = { itemsPerPage: value, page: page }; // Trigger reload of nodeList
} else if (res.fetching === false && res.error) {
throw res.error;
}
});
}
/* Derived */
const paging = $derived({ itemsPerPage, page });
const matchedNodes = $derived($nodesQuery?.data?.nodeMetricsList?.totalNodes || 0);
if (!usePaging) {
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $nodesQuery?.data != null && $nodesQuery.data?.nodeMetricsList.hasNextPage) {
let pendingPaging = { ...paging }
pendingPaging.page += 1
paging = pendingPaging
};
});
};
$: nodesQuery = queryStore({
const nodesQuery = $derived(queryStore({
client: client,
query: nodeListQuery,
variables: {
@@ -150,24 +106,92 @@
selectedResolution: selectedResolution,
},
requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change
}));
/* Effects */
$effect(() => {
if (!usePaging) {
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $nodesQuery?.data != null && $nodesQuery.data?.nodeMetricsList.hasNextPage) {
page += 1
};
});
};
});
let nodes = [];
$: if ($initialized && $nodesQuery.data) {
if (usePaging) {
nodes = [...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host));
} else {
nodes = nodes.concat([...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host)))
$effect(() => {
handleNodes($nodesQuery?.data?.nodeMetricsList?.items);
});
$effect(() => {
// Triggers (Except Paging)
from, to
selectedMetrics, selectedResolution
hostnameFilter
// Continous Scroll: Reset nodes and paging if parameters change: Existing entries will not match new selections
if (!usePaging) {
nodes = [];
page = 1;
}
});
/* Functions */
function handleNodes(data) {
if (data) {
if (usePaging || nodes.length == 0) {
nodes = [...data].sort((a, b) => a.host.localeCompare(b.host));
} else {
// Workaround to ignore secondary store triggers (reason tbd)
const oldNodes = $state.snapshot(nodes)
const newNodes = [...data].map((d) => d.host)
if (!oldNodes.some((n) => newNodes.includes(n.host))) {
nodes = nodes.concat([...data].sort((a, b) => a.host.localeCompare(b.host)))
};
};
};
};
// Decouple from Job List Paging Params?
function updateConfiguration(newItems, newPage) {
updateConfigurationMutation({
name: "plot_list_nodesPerPage",
value: newItems.toString(),
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
nodes = []; // Empty List
itemsPerPage = newItems;
page = newPage; // Trigger reload of nodeList
} else if (res.fetching === false && res.error) {
throw res.error;
}
});
}
$: if (!usePaging && (selectedMetrics || selectedResolution || hostnameFilter || from || to)) {
// Continous Scroll: Reset list and paging if parameters change: Existing entries will not match new selections
nodes = [];
paging = { itemsPerPage, page: 1 };
}
/* Const Functions */
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
$: matchedNodes = $nodesQuery.data?.nodeMetricsList.totalNodes || matchedNodes;
/* Init Header */
stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
(x) => (headerPaddingTop = x),
);
</script>
<Row>
@@ -192,7 +216,7 @@
scope="col"
style="padding-top: {headerPaddingTop}px"
>
{metric} ({systemUnits[metric]})
{metric} ({presetSystemUnits[metric]})
</th>
{/each}
</tr>
@@ -212,7 +236,7 @@
<td colspan={selectedMetrics.length + 1}> No nodes found </td>
</tr>
{/each}
{/if}
{/if}
{#if $nodesQuery.fetching || !$nodesQuery.data}
<tr>
<td colspan={selectedMetrics.length + 1}>
@@ -244,10 +268,11 @@
totalItems={matchedNodes}
on:update-paging={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
updateConfiguration(detail.itemsPerPage, detail.page);
} else {
nodes = []
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
nodes = [];
itemsPerPage = detail.itemsPerPage;
page = detail.page;
}
}}
/>

View File

@@ -10,101 +10,99 @@
<script>
import { queryStore, gql, getContextClient } from "@urql/svelte";
import { Row, Col, Card, Spinner } from "@sveltestrap/sveltestrap";
import { init, checkMetricsDisabled } from "../generic/utils.js";
import { checkMetricDisabled } from "../generic/utils.js";
import MetricPlot from "../generic/plots/MetricPlot.svelte";
export let ccconfig = null;
export let cluster = "";
export const subCluster = "";
export let selectedMetrics = null;
export let hostnameFilter = "";
export let from = null;
export let to = null;
/* Svelte 5 Props */
let {
ccconfig = null,
cluster = "",
selectedMetric = "",
hostnameFilter = "",
from = null,
to = null
} = $props();
const { query: initq } = init();
/* Const Init */
const client = getContextClient();
const nodeQuery = gql`
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(
cluster: $cluster
metrics: $metrics
from: $from
to: $to
) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit {
base
prefix
}
series {
statistics {
min
avg
max
/* Derived */
const nodesQuery = $derived(queryStore({
client: client,
query: gql`
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
nodeMetrics(
cluster: $cluster
metrics: $metrics
from: $from
to: $to
) {
host
subCluster
metrics {
name
scope
metric {
timestep
unit {
base
prefix
}
series {
statistics {
min
avg
max
}
data
}
data
}
}
}
}
}
`
$: selectedMetric = selectedMetrics[0] ? selectedMetrics[0] : "";
$: nodesQuery = queryStore({
client: client,
query: nodeQuery,
`,
variables: {
cluster: cluster,
metrics: selectedMetrics,
from: from.toISOString(),
to: to.toISOString(),
metrics: [selectedMetric],
from: from,
to: to,
},
});
}));
let rawData = []
$: if ($initq.data && $nodesQuery?.data) {
rawData = $nodesQuery?.data?.nodeMetrics.filter((h) => {
if (h.subCluster === '') { // Exclude nodes with empty subCluster field
console.warn('subCluster not configured for node', h.host)
return false
} else {
return h.metrics.some(
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
)
}
})
}
const mappedData = $derived(handleQueryData($nodesQuery?.data));
const filteredData = $derived(mappedData.filter((h) => h.host.includes(hostnameFilter)));
let mappedData = []
$: if (rawData?.length > 0) {
mappedData = rawData.map((h) => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.filter(
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
),
disabled: checkMetricsDisabled(
selectedMetrics,
cluster,
h.subCluster,
),
}))
.sort((a, b) => a.host.localeCompare(b.host))
}
let filteredData = []
$: if (mappedData?.length > 0) {
filteredData = mappedData.filter((h) =>
h.host.includes(hostnameFilter)
)
/* Functions */
function handleQueryData(queryData) {
let rawData = []
if (queryData) {
rawData = queryData.nodeMetrics.filter((h) => {
if (h.subCluster !== '') { // Exclude nodes with empty subCluster field
return h.metrics.some(
(m) => m?.name == selectedMetric && m.scope == "node",
)
};
});
};
let pendingMapped = [];
if (rawData.length > 0) {
pendingMapped = rawData.map((h) => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.filter(
(m) => m?.name == selectedMetric && m.scope == "node",
),
disabled: checkMetricDisabled(
selectedMetric,
cluster,
h.subCluster,
),
}))
.sort((a, b) => a.host.localeCompare(b.host))
}
return pendingMapped;
}
</script>
@@ -132,22 +130,25 @@
>{item.host} ({item.subCluster})</a
>
</h4>
{#if item?.disabled[selectedMetric]}
{#if item?.disabled}
<Card body class="mx-3" color="info"
>Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code
></Card
>
{:else}
<!-- "No Data"-Warning included in MetricPlot-Component -->
<MetricPlot
timestep={item.data[0].metric.timestep}
series={item.data[0].metric.series}
metric={item.data[0].name}
{cluster}
subCluster={item.subCluster}
forNode
/>
<!-- "No Data"-Warning included in MetricPlot-Component -->
<!-- #key: X-axis keeps last selected timerange otherwise -->
{#key item.data[0].metric.series[0].data.length}
<MetricPlot
timestep={item.data[0].metric.timestep}
series={item.data[0].metric.series}
metric={item.data[0].name}
{cluster}
subCluster={item.subCluster}
forNode
/>
{/key}
{/if}
</Col>
{/each}