Move common logic into systems view again

- adds backend log if subcluster for node not configured
This commit is contained in:
Christoph Kluge 2024-10-14 11:55:59 +02:00
parent 2cbe8e9517
commit 2f6e5a7648
9 changed files with 350 additions and 445 deletions

View File

@ -438,7 +438,7 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
data, err := metricDataDispatcher.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx) data, err := metricDataDispatcher.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx)
if err != nil { if err != nil {
log.Warn("Error while loading node data") log.Warn("error while loading node data")
return nil, err return nil, err
} }
@ -448,7 +448,10 @@ func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes [
Host: hostname, Host: hostname,
Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)), Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
} }
host.SubCluster, _ = archive.GetSubClusterByNode(cluster, hostname) host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
if err != nil {
log.Warnf("error in nodeMetrics resolver: %s", err)
}
for metric, scopedMetrics := range metrics { for metric, scopedMetrics := range metrics {
for _, scopedMetric := range scopedMetrics { for _, scopedMetric := range scopedMetrics {

View File

@ -9,36 +9,259 @@
--> -->
<script> <script>
import { getContext } from "svelte";
import { queryStore, gql, getContextClient } from "@urql/svelte"
import { import {
Row, Row,
Col, Col,
Card, Card,
Input,
InputGroup,
InputGroupText,
Icon,
Button,
Spinner,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
import { init, checkMetricsDisabled } from "./generic/utils.js";
import NodeOverview from "./systems/NodeOverview.svelte"; import NodeOverview from "./systems/NodeOverview.svelte";
import NodeList from "./systems/NodeList.svelte"; import NodeList from "./systems/NodeList.svelte";
import MetricSelection from "./generic/select/MetricSelection.svelte";
import TimeSelection from "./generic/select/TimeSelection.svelte";
import Refresher from "./generic/helper/Refresher.svelte";
export let displayType; export let displayType;
export let cluster; export let cluster;
export let from = null; export let from = null;
export let to = null; export let to = null;
const { query: initq } = init();
console.assert( console.assert(
displayType == "OVERVIEW" || displayType == "LIST", displayType == "OVERVIEW" || displayType == "LIST",
"Invalid nodes displayType provided!", "Invalid nodes displayType provided!",
); );
if (from == null || to == null) {
to = new Date(Date.now());
from = new Date(to.getTime());
from.setHours(from.getHours() - 12);
}
const initialized = getContext("initialized");
const ccconfig = getContext("cc-config");
const globalMetrics = getContext("globalMetrics");
const displayNodeOverview = (displayType === 'OVERVIEW')
let nodeList;
let hostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric || "";
let selectedMetrics = ccconfig.node_list_selectedMetrics || ['cpu_load', 'mem_bw', 'acc_utilization', 'net_bytes_in', 'net_bytes_out'];
let isMetricsSelectionOpen = false;
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
}
data
}
}
}
}
}
`
$: nodesQuery = queryStore({
client: client,
query: nodeQuery,
variables: {
cluster: cluster,
metrics: selectedMetrics,
from: from.toISOString(),
to: to.toISOString(),
},
});
let systemMetrics = [];
let systemUnits = {};
function loadMetrics(isInitialized) {
if (!isInitialized) return
systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
for (let sm of systemMetrics) {
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
}
}
$: loadMetrics($initialized)
$: if (displayNodeOverview) {
selectedMetrics = [selectedMetric]
}
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",
)
}
})
}
let mappedData = []
$: if (rawData?.length > 0) {
mappedData = rawData.map((h) => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.find(
(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)
)
}
</script> </script>
{#if displayType === 'OVERVIEW'} <!-- ROW1: Tools-->
<NodeOverview {cluster} {from} {to}/> <Row cols={{ xs: 2, lg: 4 }}>
{:else if displayType === 'LIST'} {#if $initq.data}
<NodeList {cluster} {from} {to}/> <!-- List Metric Select Col-->
{:else} {#if !displayNodeOverview}
<Col>
<InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metrics</InputGroupText>
<Button
outline
color="secondary"
on:click={() => (isMetricsSelectionOpen = true)}
>
Select for {cluster} ...
</Button>
</InputGroup>
</Col>
{/if}
<!-- Node Col-->
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node(s)</InputGroupText>
<Input
placeholder="Filter hostname ..."
type="text"
title="Search with at least three characters ..."
bind:value={hostnameFilter}
/>
</InputGroup>
</Col>
<!-- Range Col-->
<Col>
<TimeSelection bind:from bind:to />
</Col>
<!-- Overview Metric Col-->
{#if displayNodeOverview}
<Col class="mt-2 mt-lg-0">
<InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText>
<Input type="select" bind:value={selectedMetric}>
{#each systemMetrics as metric}
<option value={metric.name}
>{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
>
{/each}
</Input>
</InputGroup>
</Col>
{/if}
<!-- Refresh Col-->
<Col class="mt-2 mt-lg-0">
<Refresher
on:refresh={() => {
const diff = Date.now() - to;
from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff);
}}
/>
</Col>
{/if}
</Row>
<!-- ROW2: Content-->
{#if displayType !== "OVERVIEW" && displayType !== "LIST"}
<Row> <Row>
<Col> <Col>
<Card color="danger"> <Card body color="danger">Unknown displayList type! </Card>
Unknown displayList type!
</Card>
</Col> </Col>
</Row> </Row>
{:else if $nodesQuery.error}
<Row>
<Col>
<Card body color="danger">{$nodesQuery.error.message}</Card>
</Col>
</Row>
{:else if $nodesQuery.fetching }
<Row>
<Col>
<Spinner />
</Col>
</Row>
{:else if $initialized && $nodesQuery?.data}
{#if displayNodeOverview}
<!-- ROW2-1: Node Overview (Grid Included)-->
<NodeOverview {ccconfig} {cluster} bind:selectedMetric data={filteredData}/>
{:else}
<!-- ROW2-2: Node List-->
<Row>
<Col>
<!-- <NodeList bind:nodesData={$nodesQuery.data} {cluster} {selectedMetrics} {systemUnits} bind:hostnameFilter/> -->
</Col>
</Row>
<MetricSelection
{cluster}
configName="node_list_selectedMetrics"
bind:metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen}
/>
{/if}
{/if} {/if}

View File

@ -22,10 +22,10 @@
function tile(items, itemsPerRow) { function tile(items, itemsPerRow) {
const rows = [] const rows = []
for (let ri = 0; ri < items.length; ri += itemsPerRow) { for (let ri = 0; ri < items?.length; ri += itemsPerRow) {
const row = [] const row = []
for (let ci = 0; ci < itemsPerRow; ci += 1) { for (let ci = 0; ci < itemsPerRow; ci += 1) {
if (ri + ci < items.length) if (ri + ci < items?.length)
row.push(items[ri + ci]) row.push(items[ri + ci])
else else
row.push({ _is_placeholder: true, ri, ci }) row.push({ _is_placeholder: true, ri, ci })

View File

@ -227,7 +227,7 @@
function update(u) { function update(u) {
const { left, top } = u.cursor; const { left, top } = u.cursor;
const width = u.over.querySelector(".u-legend").offsetWidth; const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
legendEl.style.transform = legendEl.style.transform =
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)"; "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
} }

View File

@ -303,8 +303,19 @@ export function stickyHeader(datatableHeaderSelector, updatePading) {
export function checkMetricDisabled(m, c, s) { // [m]etric, [c]luster, [s]ubcluster export function checkMetricDisabled(m, c, s) { // [m]etric, [c]luster, [s]ubcluster
const metrics = getContext("globalMetrics"); const metrics = getContext("globalMetrics");
const result = metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s) const available = metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s)
return !result // Return inverse logic
return !available
}
export function checkMetricsDisabled(ma, c, s) { // [m]etric[a]rray, [c]luster, [s]ubcluster
let result = {};
const metrics = getContext("globalMetrics");
ma.forEach((m) => {
// Return named inverse logic: !available
result[m] = !(metrics?.find((gm) => gm.name === m)?.availability?.find((av) => av.cluster === c)?.subClusters?.includes(s))
});
return result
} }
export function getStatsItems() { export function getStatsItems() {

View File

@ -1,5 +1,10 @@
<!-- <!--
@component Main jobList component; lists jobs according to set filters @component Cluster Per Node List component; renders current state of SELECTABLE metrics for ALL nodes
Properties:
- `cluster String`: The cluster to show status information for
- `from Date?`: Custom Time Range selection 'from' [Default: null]
- `to Date?`: Custom Time Range selection 'to' [Default: null]
Properties: Properties:
- `sorting Object?`: Currently active sorting [Default: {field: "startTime", type: "col", order: "DESC"}] - `sorting Object?`: Currently active sorting [Default: {field: "startTime", type: "col", order: "DESC"}]
@ -14,134 +19,46 @@
--> -->
<script> <script>
import { getContext } from "svelte";
import { import {
queryStore,
gql, gql,
getContextClient,
mutationStore, mutationStore,
} from "@urql/svelte"; } from "@urql/svelte";
import { Row, Table, Card, Spinner } from "@sveltestrap/sveltestrap"; import { Row, Table } from "@sveltestrap/sveltestrap";
import { stickyHeader } from "../generic/utils.js"; import {
import Pagination from "../generic/joblist/Pagination.svelte"; checkMetricsDisabled,
import JobListRow from "../generic/joblist/JobListRow.svelte"; stickyHeader
} from "../generic/utils.js";
import NodeListRow from "./nodelist/NodeListRow.svelte";
const ccconfig = getContext("cc-config"), export let cluster;
initialized = getContext("initialized"), export let nodesData = null;
globalMetrics = getContext("globalMetrics"); export let selectedMetrics = [];
export let systemUnits = null;
export let hostnameFilter = "";
export let sorting = { field: "startTime", type: "col", order: "DESC" }; // Always use ONE BIG list, but: Make copyable markers -> Nodeinfo ! (like in markdown)
export let matchedJobs = 0;
export let metrics = ccconfig.plot_list_selectedMetrics;
export let showFootprint;
let usePaging = ccconfig.job_list_usePaging $: nodes = nodesData.nodeMetrics
let itemsPerPage = usePaging ? ccconfig.plot_list_jobsPerPage : 10; .filter(
let page = 1; (h) =>
let paging = { itemsPerPage, page }; h.host.includes(hostnameFilter) &&
let filter = []; h.metrics.some(
let triggerMetricRefresh = false; (m) => selectedMetrics.includes(m.name) && m.scope == "node",
),
function getUnit(m) { )
const rawUnit = globalMetrics.find((gm) => gm.name === m)?.unit .map((h) => ({
return (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "") host: h.host,
} subCluster: h.subCluster,
data: h.metrics.find(
const client = getContextClient(); (m) => selectedMetrics.includes(m.name) && m.scope == "node",
const query = gql` ),
query ( disabled: checkMetricsDisabled(
$filter: [JobFilter!]! selectedMetrics,
$sorting: OrderByInput! cluster,
$paging: PageRequest! h.subCluster,
) { ),
jobs(filter: $filter, order: $sorting, page: $paging) { }))
items { .sort((a, b) => a.host.localeCompare(b.host))
id
jobId
user
project
cluster
subCluster
startTime
duration
numNodes
numHWThreads
numAcc
walltime
resources {
hostname
}
SMT
exclusive
partition
arrayJobId
monitoringStatus
state
tags {
id
type
name
scope
}
userData {
name
}
metaData
footprint {
name
stat
value
}
}
count
hasNextPage
}
}
`;
$: jobsStore = queryStore({
client: client,
query: query,
variables: { paging, sorting, filter },
});
let jobs = []
$: if ($initialized && $jobsStore.data) {
jobs = [...$jobsStore.data.jobs.items]
}
$: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
export function refreshJobs() {
jobsStore = queryStore({
client: client,
query: query,
variables: { paging, sorting, filter },
requestPolicy: "network-only",
});
}
export function refreshAllMetrics() {
// Refresh Job Metrics (Downstream will only query for running jobs)
triggerMetricRefresh = true
setTimeout(function () {
triggerMetricRefresh = false;
}, 100);
}
// (Re-)query and optionally set new filters; Query will be started reactively.
export function queryJobs(filters) {
if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor });
}
filter = filters;
}
page = 1;
paging = paging = { page, itemsPerPage };
}
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
@ -155,53 +72,19 @@
}); });
}; };
function updateConfiguration(value, page) { function updateConfiguration(value) {
updateConfigurationMutation({ updateConfigurationMutation({
name: "plot_list_jobsPerPage", name: "node_list_selectedMetrics",
value: value, value: value,
}).subscribe((res) => { }).subscribe((res) => {
if (res.fetching === false && !res.error) { if (res.fetching === false && !res.error) {
jobs = [] // Empty List console.log('Selected Metrics for Node List Updated')
paging = { itemsPerPage: value, page: page }; // Trigger reload of jobList
} else if (res.fetching === false && res.error) { } else if (res.fetching === false && res.error) {
throw res.error; throw res.error;
} }
}); });
} }
if (!usePaging) {
let scrollMultiplier = 1
window.addEventListener('scroll', () => {
let {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
// Add 100 px offset to trigger load earlier
if (scrollTop + clientHeight >= scrollHeight - 100 && $jobsStore.data != null && $jobsStore.data.jobs.hasNextPage) {
let pendingPaging = { ...paging }
scrollMultiplier += 1
pendingPaging.itemsPerPage = itemsPerPage * scrollMultiplier
paging = pendingPaging
};
});
};
let plotWidth = null;
let tableWidth = null;
let jobInfoColumnWidth = 250;
$: if (showFootprint) {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / (metrics.length + 1) - 10,
);
} else {
plotWidth = Math.floor(
(tableWidth - jobInfoColumnWidth) / metrics.length - 10,
);
}
let headerPaddingTop = 0; let headerPaddingTop = 0;
stickyHeader( stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)", ".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
@ -210,89 +93,43 @@
</script> </script>
<Row> <Row>
<div class="col cc-table-wrapper" bind:clientWidth={tableWidth}> <div class="col cc-table-wrapper">
<Table cellspacing="0px" cellpadding="0px"> <Table cellspacing="0px" cellpadding="0px">
<thead> <thead>
<tr> <tr>
<th <th
class="position-sticky top-0" class="position-sticky top-0"
scope="col" scope="col"
style="width: {jobInfoColumnWidth}px; padding-top: {headerPaddingTop}px" style="padding-top: {headerPaddingTop}px"
> >
Job Info Node Info
</th> </th>
{#if showFootprint}
<th {#each selectedMetrics as metric (metric)}
class="position-sticky top-0"
scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px"
>
Job Footprint
</th>
{/if}
{#each metrics as metric (metric)}
<th <th
class="position-sticky top-0 text-center" class="position-sticky top-0 text-center"
scope="col" scope="col"
style="width: {plotWidth}px; padding-top: {headerPaddingTop}px" style="padding-top: {headerPaddingTop}px"
> >
{metric} {metric} ({systemUnits[metric]})
{#if $initialized}
({getUnit(metric)})
{/if}
</th> </th>
{/each} {/each}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#if $jobsStore.error} {#each nodes as node (node)}
<tr> {node}
<td colspan={metrics.length + 1}> <!-- <NodeListRow {node} {selectedMetrics} /> -->
<Card body color="danger" class="mb-3"
><h2>{$jobsStore.error.message}</h2></Card
>
</td>
</tr>
{:else}
{#each jobs as job (job)}
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} />
{:else} {:else}
<tr> <tr>
<td colspan={metrics.length + 1}> No jobs found </td> <td>No nodes found </td>
</tr> </tr>
{/each} {/each}
{/if}
{#if $jobsStore.fetching || !$jobsStore.data}
<tr>
<td colspan={metrics.length + 1}>
<div style="text-align:center;">
<Spinner secondary />
</div>
</td>
</tr>
{/if}
</tbody> </tbody>
</Table> </Table>
</div> </div>
</Row> </Row>
{#if usePaging}
<Pagination
bind:page
{itemsPerPage}
itemText="Jobs"
totalItems={matchedJobs}
on:update-paging={({ detail }) => {
if (detail.itemsPerPage != itemsPerPage) {
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
} else {
jobs = []
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
}
}}
/>
{/if}
<style> <style>
.cc-table-wrapper { .cc-table-wrapper {
overflow: initial; overflow: initial;

View File

@ -2,201 +2,31 @@
@component Cluster Per Node Overview component; renders current state of ONE metric for ALL nodes @component Cluster Per Node Overview component; renders current state of ONE metric for ALL nodes
Properties: Properties:
- `ccconfig Object?`: The ClusterCockpit Config Context [Default: null]
- `data Object?`: The GQL nodeMetrics data [Default: null]
- `cluster String`: The cluster to show status information for - `cluster String`: The cluster to show status information for
- `from Date?`: Custom Time Range selection 'from' [Default: null] - `selectedMetric String?`: The selectedMetric input [Default: ""]
- `to Date?`: Custom Time Range selection 'to' [Default: null]
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext } from "svelte";
import { import { Card } from "@sveltestrap/sveltestrap";
Row,
Col,
Input,
InputGroup,
InputGroupText,
Icon,
Spinner,
Card,
} from "@sveltestrap/sveltestrap";
import {
queryStore,
gql,
getContextClient,
} from "@urql/svelte";
import {
init,
checkMetricDisabled,
} from "../generic/utils.js";
import PlotGrid from "../generic/PlotGrid.svelte"; import PlotGrid from "../generic/PlotGrid.svelte";
import MetricPlot from "../generic/plots/MetricPlot.svelte"; import MetricPlot from "../generic/plots/MetricPlot.svelte";
import TimeSelection from "../generic/select/TimeSelection.svelte";
import Refresher from "../generic/helper/Refresher.svelte";
export let cluster; export let ccconfig = null;
export let from = null; export let data = null;
export let to = null; export let cluster = "";
export let selectedMetric = "";
const { query: initq } = init();
if (from == null || to == null) {
to = new Date(Date.now());
from = new Date(to.getTime());
from.setHours(from.getHours() - 12);
}
const initialized = getContext("initialized");
const ccconfig = getContext("cc-config");
const clusters = getContext("clusters"); const clusters = getContext("clusters");
const globalMetrics = getContext("globalMetrics");
let hostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric;
const client = getContextClient();
$: nodesQuery = 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
}
}
}
}
}
`,
variables: {
cluster: cluster,
metrics: [selectedMetric],
from: from.toISOString(),
to: to.toISOString(),
},
});
let systemMetrics = [];
let systemUnits = {};
function loadMetrics(isInitialized) {
if (!isInitialized) return
systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
for (let sm of systemMetrics) {
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
}
}
$: loadMetrics($initialized)
</script> </script>
<Row cols={{ xs: 2, lg: 4 }}>
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq.fetching}
<Spinner />
{:else}
<!-- Node Col-->
<Col>
<InputGroup>
<InputGroupText><Icon name="hdd" /></InputGroupText>
<InputGroupText>Find Node</InputGroupText>
<Input
placeholder="hostname..."
type="text"
bind:value={hostnameFilter}
/>
</InputGroup>
</Col>
<!-- Range Col-->
<Col>
<TimeSelection bind:from bind:to />
</Col>
<!-- Metric Col-->
<Col class="mt-2 mt-lg-0">
<InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metric</InputGroupText>
<select class="form-select" bind:value={selectedMetric}>
{#each systemMetrics as metric}
<option value={metric.name}
>{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
>
{/each}
</select>
</InputGroup>
</Col>
<!-- Refresh Col-->
<Col class="mt-2 mt-lg-0">
<Refresher
on:refresh={() => {
const diff = Date.now() - to;
from = new Date(from.getTime() + diff);
to = new Date(to.getTime() + diff);
}}
/>
</Col>
{/if}
</Row>
<br />
{#if $nodesQuery.error}
<Row>
<Col>
<Card body color="danger">{$nodesQuery.error.message}</Card>
</Col>
</Row>
{:else if $nodesQuery.fetching || $initq.fetching}
<Row>
<Col>
<Spinner />
</Col>
</Row>
{:else}
<PlotGrid <PlotGrid
let:item let:item
renderFor="systems" renderFor="systems"
itemsPerRow={ccconfig.plot_view_plotsPerRow} itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodesQuery.data.nodeMetrics items={data}
.filter(
(h) =>
h.host.includes(hostnameFilter) &&
h.metrics.some(
(m) => m.name == selectedMetric && m.scope == "node",
),
)
.map((h) => ({
host: h.host,
subCluster: h.subCluster,
data: h.metrics.find(
(m) => m.name == selectedMetric && m.scope == "node",
),
disabled: checkMetricDisabled(
selectedMetric,
cluster,
h.subCluster,
),
}))
.sort((a, b) => a.host.localeCompare(b.host))}
> >
<h4 style="width: 100%; text-align: center;"> <h4 style="width: 100%; text-align: center;">
<a <a
@ -205,7 +35,14 @@
>{item.host} ({item.subCluster})</a >{item.host} ({item.subCluster})</a
> >
</h4> </h4>
{#if item.disabled === false && item.data} {#if item?.data}
{#if item?.disabled[selectedMetric]}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
>Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code
></Card
>
{:else}
<MetricPlot <MetricPlot
timestep={item.data.metric.timestep} timestep={item.data.metric.timestep}
series={item.data.metric.series} series={item.data.metric.series}
@ -214,12 +51,7 @@
subCluster={item.subCluster} subCluster={item.subCluster}
forNode={true} forNode={true}
/> />
{:else if item.disabled === true && item.data} {/if}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
>Metric disabled for subcluster <code
>{selectedMetric}:{item.subCluster}</code
></Card
>
{:else} {:else}
<Card <Card
style="margin-left: 2rem;margin-right: 2rem;" style="margin-left: 2rem;margin-right: 2rem;"
@ -229,4 +61,3 @@
> >
{/if} {/if}
</PlotGrid> </PlotGrid>
{/if}

View File

@ -1,5 +1,5 @@
<!-- <!--
@component Displays job metaData, serves links to detail pages @component Displays node info, serves link to signle node page
Properties: Properties:
- `job Object`: The Job Object (GraphQL.Job) - `job Object`: The Job Object (GraphQL.Job)

View File

@ -1,5 +1,5 @@
<!-- <!--
@component Data row for a single job displaying metric plots @component Data row for a single node displaying metric plots
Properties: Properties:
- `job Object`: The job object (GraphQL.Job) - `job Object`: The job object (GraphQL.Job)
@ -15,9 +15,9 @@
import { getContext } from "svelte"; import { getContext } from "svelte";
import { Card, Spinner } from "@sveltestrap/sveltestrap"; import { Card, Spinner } from "@sveltestrap/sveltestrap";
import { maxScope, checkMetricDisabled } from "../../generic/utils.js"; import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
import JobInfo from "./NodeInfo.svelte";
import MetricPlot from "../../generic/plots/MetricPlot.svelte"; import MetricPlot from "../../generic/plots/MetricPlot.svelte";
import JobFootprint from "../../generic/helper/JobFootprint.svelte";
import NodeInfo from "./NodeInfo.svelte";
export let job; export let job;
export let metrics; export let metrics;