Finish prototype implementation of nodelist view

This commit is contained in:
Christoph Kluge 2024-10-14 18:37:48 +02:00
parent 2f6e5a7648
commit 673fdc443c
7 changed files with 230 additions and 429 deletions

View File

@ -44,7 +44,7 @@
if (from == null || to == null) { if (from == null || to == null) {
to = new Date(Date.now()); to = new Date(Date.now());
from = new Date(to.getTime()); from = new Date(to.getTime());
from.setHours(from.getHours() - 12); from.setHours(from.getHours() - 4);
} }
const initialized = getContext("initialized") const initialized = getContext("initialized")
@ -156,15 +156,17 @@
<InputGroup> <InputGroup>
<InputGroupText><Icon name="activity" /></InputGroupText> <InputGroupText><Icon name="activity" /></InputGroupText>
<InputGroupText>Activity</InputGroupText> <InputGroupText>Activity</InputGroupText>
<Input style="background-color: white;"type="text" value="{$nodeJobsData.data.jobs.count} Jobs" disabled/> <Input style="background-color: white;" type="text" value="{$nodeJobsData.data.jobs.count} Jobs" disabled/>
<a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-secondary" role="button" aria-disabled="true"> <a title="Show jobs running on this node" href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank" class="btn btn-outline-secondary" role="button" aria-disabled="true">
<Icon name="view-list" /> Show List <Icon name="view-list" /> Show List
</a> </a>
</InputGroup> </InputGroup>
{:else} {:else}
<Input type="text" disabled> <InputGroup>
No currently running jobs. <InputGroupText><Icon name="activity" /></InputGroupText>
</Input> <InputGroupText>Activity</InputGroupText>
<Input type="text" value="No running jobs." disabled />
</InputGroup>
{/if} {/if}
</Col> </Col>
<!-- Refresh Col--> <!-- Refresh Col-->

View File

@ -45,7 +45,7 @@
if (from == null || to == null) { if (from == null || to == null) {
to = new Date(Date.now()); to = new Date(Date.now());
from = new Date(to.getTime()); from = new Date(to.getTime());
from.setHours(from.getHours() - 12); from.setHours(from.getHours() - 4);
} }
const initialized = getContext("initialized"); const initialized = getContext("initialized");
@ -53,12 +53,15 @@
const globalMetrics = getContext("globalMetrics"); const globalMetrics = getContext("globalMetrics");
const displayNodeOverview = (displayType === 'OVERVIEW') const displayNodeOverview = (displayType === 'OVERVIEW')
let nodeList;
let hostnameFilter = ""; let hostnameFilter = "";
let selectedMetric = ccconfig.system_view_selectedMetric || ""; 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 selectedMetrics = ccconfig[`node_list_selectedMetrics:${cluster}`] || [ccconfig.system_view_selectedMetric];
let isMetricsSelectionOpen = false; let isMetricsSelectionOpen = false;
// Todo: Add Idle State Filter (== No allocated Jobs)
// Todo: NodeList: Mindestens Accelerator Scope ... "Show Detail" Switch?
// Todo: Review performance // observed high client-side load frequency
const client = getContextClient(); const client = getContextClient();
const nodeQuery = gql` const nodeQuery = gql`
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) { query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
@ -139,7 +142,7 @@
mappedData = rawData.map((h) => ({ mappedData = rawData.map((h) => ({
host: h.host, host: h.host,
subCluster: h.subCluster, subCluster: h.subCluster,
data: h.metrics.find( data: h.metrics.filter(
(m) => selectedMetrics.includes(m.name) && m.scope == "node", (m) => selectedMetrics.includes(m.name) && m.scope == "node",
), ),
disabled: checkMetricsDisabled( disabled: checkMetricsDisabled(
@ -157,24 +160,23 @@
h.host.includes(hostnameFilter) h.host.includes(hostnameFilter)
) )
} }
</script> </script>
<!-- ROW1: Tools--> <!-- ROW1: Tools-->
<Row cols={{ xs: 2, lg: 4 }}> <Row cols={{ xs: 2, lg: 4 }} class="mb-3">
{#if $initq.data} {#if $initq.data}
<!-- List Metric Select Col--> <!-- List Metric Select Col-->
{#if !displayNodeOverview} {#if !displayNodeOverview}
<Col> <Col>
<InputGroup> <InputGroup>
<InputGroupText><Icon name="graph-up" /></InputGroupText> <InputGroupText><Icon name="graph-up" /></InputGroupText>
<InputGroupText>Metrics</InputGroupText> <InputGroupText class="text-capitalize">Metrics</InputGroupText>
<Button <Button
outline outline
color="secondary" color="primary"
on:click={() => (isMetricsSelectionOpen = true)} on:click={() => (isMetricsSelectionOpen = true)}
> >
Select for {cluster} ... {selectedMetrics.length} selected
</Button> </Button>
</InputGroup> </InputGroup>
</Col> </Col>
@ -187,7 +189,6 @@
<Input <Input
placeholder="Filter hostname ..." placeholder="Filter hostname ..."
type="text" type="text"
title="Search with at least three characters ..."
bind:value={hostnameFilter} bind:value={hostnameFilter}
/> />
</InputGroup> </InputGroup>
@ -247,21 +248,19 @@
{:else if $initialized && $nodesQuery?.data} {:else if $initialized && $nodesQuery?.data}
{#if displayNodeOverview} {#if displayNodeOverview}
<!-- ROW2-1: Node Overview (Grid Included)--> <!-- ROW2-1: Node Overview (Grid Included)-->
<NodeOverview {ccconfig} {cluster} bind:selectedMetric data={filteredData}/> <NodeOverview {cluster} {ccconfig} data={filteredData} bind:selectedMetric/>
{:else} {:else}
<!-- ROW2-2: Node List--> <!-- ROW2-2: Node List (Grid Included)-->
<Row> <NodeList {cluster} {selectedMetrics} {systemUnits} data={filteredData}/>
<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} {/if}
<MetricSelection
{cluster}
configName="node_list_selectedMetrics"
metrics={selectedMetrics}
bind:isOpen={isMetricsSelectionOpen}
on:update-metrics={({ detail }) => {
selectedMetrics = [...detail]
}}
/>

View File

@ -12,7 +12,7 @@
--> -->
<script> <script>
import { getContext } from "svelte"; import { getContext, createEventDispatcher } from "svelte";
import { import {
Modal, Modal,
ModalBody, ModalBody,
@ -33,6 +33,7 @@
const onInit = getContext("on-init") const onInit = getContext("on-init")
const globalMetrics = getContext("globalMetrics") const globalMetrics = getContext("globalMetrics")
const dispatch = createEventDispatcher();
let newMetricsOrder = []; let newMetricsOrder = [];
let unorderedMetrics = [...metrics]; let unorderedMetrics = [...metrics];
@ -128,6 +129,8 @@
throw res.error; throw res.error;
} }
}); });
dispatch('update-metrics', metrics);
} }
</script> </script>

View File

@ -5,86 +5,22 @@
- `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] - `from Date?`: Custom Time Range selection 'from' [Default: null]
- `to Date?`: Custom Time Range selection 'to' [Default: null] - `to Date?`: Custom Time Range selection 'to' [Default: null]
Properties:
- `sorting Object?`: Currently active sorting [Default: {field: "startTime", type: "col", order: "DESC"}]
- `matchedJobs Number?`: Number of matched jobs for selected filters [Default: 0]
- `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection]
- `showFootprint Bool`: If to display the jobFootprint component
Functions:
- `refreshJobs()`: Load jobs data with unchanged parameters and 'network-only' keyword
- `refreshAllMetrics()`: Trigger downstream refresh of all running jobs' metric data
- `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1
--> -->
<script> <script>
import {
gql,
mutationStore,
} from "@urql/svelte";
import { Row, Table } from "@sveltestrap/sveltestrap"; import { Row, Table } from "@sveltestrap/sveltestrap";
import { import {
checkMetricsDisabled,
stickyHeader stickyHeader
} from "../generic/utils.js"; } from "../generic/utils.js";
import NodeListRow from "./nodelist/NodeListRow.svelte"; import NodeListRow from "./nodelist/NodeListRow.svelte";
export let cluster; export let cluster;
export let nodesData = null; export let data = null;
export let selectedMetrics = []; export let selectedMetrics = [];
export let systemUnits = null; export let systemUnits = null;
export let hostnameFilter = "";
// Always use ONE BIG list, but: Make copyable markers -> Nodeinfo ! (like in markdown) // Always use ONE BIG list, but: Make copyable markers -> Nodeinfo ! (like in markdown)
$: nodes = nodesData.nodeMetrics
.filter(
(h) =>
h.host.includes(hostnameFilter) &&
h.metrics.some(
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
),
)
.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))
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
function updateConfiguration(value) {
updateConfigurationMutation({
name: "node_list_selectedMetrics",
value: value,
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
console.log('Selected Metrics for Node List Updated')
} else if (res.fetching === false && res.error) {
throw res.error;
}
});
}
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)",
@ -98,11 +34,11 @@
<thead> <thead>
<tr> <tr>
<th <th
class="position-sticky top-0" class="position-sticky top-0 text-capitalize"
scope="col" scope="col"
style="padding-top: {headerPaddingTop}px" style="padding-top: {headerPaddingTop}px"
> >
Node Info {cluster} Node Info
</th> </th>
{#each selectedMetrics as metric (metric)} {#each selectedMetrics as metric (metric)}
@ -117,9 +53,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each nodes as node (node)} {#each data as nodeData (nodeData)}
{node} <NodeListRow {nodeData} {cluster} {selectedMetrics}/>
<!-- <NodeListRow {node} {selectedMetrics} /> -->
{:else} {:else}
<tr> <tr>
<td>No nodes found </td> <td>No nodes found </td>

View File

@ -35,7 +35,7 @@
>{item.host} ({item.subCluster})</a >{item.host} ({item.subCluster})</a
> >
</h4> </h4>
{#if item?.data} {#if item?.data[0]}
{#if item?.disabled[selectedMetric]} {#if item?.disabled[selectedMetric]}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info" <Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
>Metric disabled for subcluster <code >Metric disabled for subcluster <code
@ -44,9 +44,9 @@
> >
{:else} {:else}
<MetricPlot <MetricPlot
timestep={item.data.metric.timestep} timestep={item.data[0].metric.timestep}
series={item.data.metric.series} series={item.data[0].metric.series}
metric={item.data.name} metric={item.data[0].name}
cluster={clusters.find((c) => c.name == cluster)} cluster={clusters.find((c) => c.name == cluster)}
subCluster={item.subCluster} subCluster={item.subCluster}
forNode={true} forNode={true}

View File

@ -7,146 +7,159 @@
--> -->
<script> <script>
import { Badge, Icon } from "@sveltestrap/sveltestrap"; import {
import { scrambleNames, scramble } from "../../generic/utils.js"; Spinner,
import Tag from "../../generic/helper/Tag.svelte"; Icon,
import TagManagement from "../../generic/helper/TagManagement.svelte"; Button,
Card,
CardHeader,
CardBody,
Input,
InputGroup,
InputGroupText, } from "@sveltestrap/sveltestrap";
import {
queryStore,
gql,
getContextClient,
} from "@urql/svelte";
export let job; export let cluster;
export let jobTags = job.tags; export let subCluster
export let showTagedit = false; export let hostname;
export let username = null;
export let authlevel= null;
export let roles = null;
function formatDuration(duration) { const client = getContextClient();
const hours = Math.floor(duration / 3600); const paging = { itemsPerPage: 50, page: 1 };
duration -= hours * 3600; const sorting = { field: "startTime", type: "col", order: "DESC" };
const minutes = Math.floor(duration / 60); const filter = [
duration -= minutes * 60; { cluster: { eq: cluster } },
const seconds = duration; { node: { contains: hostname } },
return `${hours}:${("0" + minutes).slice(-2)}:${("0" + seconds).slice(-2)}`; { state: ["running"] },
} ];
function getStateColor(state) { const nodeJobsQuery = gql`
switch (state) { query (
case "running": $filter: [JobFilter!]!
return "success"; $sorting: OrderByInput!
case "completed": $paging: PageRequest!
return "primary"; ) {
default: jobs(filter: $filter, order: $sorting, page: $paging) {
return "danger"; count
} }
} }
`;
$: nodeJobsData = queryStore({
client: client,
query: nodeJobsQuery,
variables: { paging, sorting, filter },
});
</script> </script>
<div> <Card class="pb-2">
<p class="mb-2"> <CardHeader class="d-inline-flex justify-content-between align-items-end">
<span class="fw-bold" <div>
><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a> <h5 class="mb-0">
({job.cluster})</span Node
> <a href="/monitoring/node/{cluster}/{hostname}" target="_blank">
{#if job.metaData?.jobName} {hostname}
<br /> </a>
{#if job.metaData?.jobName.length <= 25} </h5>
<div>{job.metaData.jobName}</div>
{:else}
<div
class="truncate"
style="cursor:help; width:230px;"
title={job.metaData.jobName}
>
{job.metaData.jobName}
</div> </div>
{/if} <div class="text-capitalize">
{/if} <h6 class="mb-0">
{#if job.arrayJobId} {cluster} {subCluster}
Array Job: <a </h6>
href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}" </div>
target="_blank">#{job.arrayJobId}</a </CardHeader>
> <CardBody>
{#if $nodeJobsData.fetching}
<Spinner />
{:else if $nodeJobsData.data}
<p>
{#if $nodeJobsData.data.jobs.count > 0}
<InputGroup>
<InputGroupText>
<Icon name="circle-fill"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="success" disabled>
Allocated
</Button>
</InputGroup>
{:else}
<InputGroup>
<InputGroupText>
<Icon name="circle"/>
</InputGroupText>
<InputGroupText>
Status
</InputGroupText>
<Button color="secondary" disabled>
Idle
</Button>
</InputGroup>
{/if} {/if}
</p> </p>
<hr class="mt-0 mb-3"/>
<p class="mb-2"> <p>
<Icon name="person-fill" /> {#if $nodeJobsData.data.jobs.count > 0}
<a class="fst-italic" href="/monitoring/user/{job.user}" target="_blank"> <InputGroup class="justify-content-between">
{scrambleNames ? scramble(job.user) : job.user} <InputGroupText>
<Icon name="activity"/>
</InputGroupText>
<InputGroupText>
Activity
</InputGroupText>
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{$nodeJobsData.data.jobs.count} Jobs" 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" />
Show List
</a> </a>
{#if job.userData && job.userData.name} </InputGroup>
({scrambleNames ? scramble(job.userData.name) : job.userData.name}) {:else}
<InputGroup class="justify-content-between">
<InputGroupText>
<Icon name="activity" />
</InputGroupText>
<InputGroupText>
Activity
</InputGroupText>
<Input class="flex-grow-1" type="text" style="background-color: white;" value="No running jobs." disabled />
</InputGroup>
{/if} {/if}
{#if job.project && job.project != "no project"} </p>
<br /> <p>
<Icon name="people-fill" /> <InputGroup class="justify-content-between">
<a <InputGroupText>
class="fst-italic" <Icon name="people"/>
href="/monitoring/jobs/?project={job.project}&projectMatch=eq" </InputGroupText>
target="_blank" <InputGroupText class="flex-fill">
> Users on Node
{scrambleNames ? scramble(job.project) : job.project} </InputGroupText>
<a title="Show jobs running 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" />
Show List
</a> </a>
{/if} </InputGroup>
</p> </p>
<p>
<InputGroup class="justify-content-between">
<InputGroupText>
<Icon name="journals"/>
</InputGroupText>
<InputGroupText class="flex-fill">
Projects on Node
</InputGroupText>
<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" />
Show List
</a>
</InputGroup>
</p>
{/if}
</CardBody>
</Card>
<p class="mb-2">
{#if job.numNodes == 1}
{job.resources[0].hostname}
{:else}
{job.numNodes}
{/if}
<Icon name="pc-horizontal" />
{#if job.exclusive != 1}
(shared)
{/if}
{#if job.numAcc > 0}
, {job.numAcc} <Icon name="gpu-card" />
{/if}
{#if job.numHWThreads > 0}
, {job.numHWThreads} <Icon name="cpu" />
{/if}
<br />
{job.subCluster}
</p>
<p class="mb-2">
Start: <span class="fw-bold"
>{new Date(job.startTime).toLocaleString()}</span
>
<br />
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span>
<Badge color={getStateColor(job.state)}>{job.state}</Badge>
{#if job.walltime}
<br />
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
{/if}
</p>
{#if showTagedit}
<hr class="mt-0 mb-2"/>
<p class="mb-1">
<TagManagement bind:jobTags {job} {username} {authlevel} {roles} renderModal/> :
{#if jobTags?.length > 0}
{#each jobTags as tag}
<Tag {tag}/>
{/each}
{:else}
<span style="font-size: 0.9rem; background-color: lightgray;" class="my-1 badge text-dark">No Tags</span>
{/if}
</p>
{:else}
<p class="mb-1">
{#each jobTags as tag}
<Tag {tag} />
{/each}
</p>
{/if}
</div>
<style>
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -5,203 +5,52 @@
- `job Object`: The job object (GraphQL.Job) - `job Object`: The job object (GraphQL.Job)
- `metrics [String]`: Currently selected metrics - `metrics [String]`: Currently selected metrics
- `plotWidth Number`: Width of the sub-components - `plotWidth Number`: Width of the sub-components
- `plotHeight Number?`: Height of the sub-components [Default: 275]
- `showFootprint Bool`: Display of footprint component for job
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query
--> -->
<script> <script>
import { queryStore, gql, getContextClient } from "@urql/svelte"; import { Card } from "@sveltestrap/sveltestrap";
import { getContext } from "svelte";
import { Card, Spinner } from "@sveltestrap/sveltestrap";
import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
import MetricPlot from "../../generic/plots/MetricPlot.svelte"; import MetricPlot from "../../generic/plots/MetricPlot.svelte";
import NodeInfo from "./NodeInfo.svelte"; import NodeInfo from "./NodeInfo.svelte";
export let job; export let cluster;
export let metrics; export let nodeData;
export let plotWidth; export let selectedMetrics;
export let plotHeight = 275;
export let showFootprint;
export let triggerMetricRefresh = false;
const resampleConfig = getContext("resampling") || null;
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
let { id } = job;
let scopes = job.numNodes == 1
? job.numAcc >= 1
? ["core", "accelerator"]
: ["core"]
: ["node"];
let selectedResolution = resampleDefault;
let zoomStates = {};
const cluster = getContext("clusters").find((c) => c.name == job.cluster);
const client = getContextClient();
const query = gql`
query ($id: ID!, $metrics: [String!]!, $scopes: [MetricScope!]!, $selectedResolution: Int) {
jobMetrics(id: $id, metrics: $metrics, scopes: $scopes, resolution: $selectedResolution) {
name
scope
metric {
unit {
prefix
base
}
timestep
statisticsSeries {
min
mean
median
max
}
series {
hostname
id
data
statistics {
min
avg
max
}
}
}
}
}
`;
function handleZoom(detail, metric) {
if ( // States have to differ, causes deathloop if just set
(zoomStates[metric]?.x?.min !== detail?.lastZoomState?.x?.min) &&
(zoomStates[metric]?.y?.max !== detail?.lastZoomState?.y?.max)
) {
zoomStates[metric] = {...detail.lastZoomState}
}
if (detail?.newRes) { // Triggers GQL
selectedResolution = detail.newRes
}
}
$: metricsQuery = queryStore({
client: client,
query: query,
variables: { id, metrics, scopes, selectedResolution },
});
function refreshMetrics() {
metricsQuery = queryStore({
client: client,
query: query,
variables: { id, metrics, scopes, selectedResolution },
// requestPolicy: 'network-only' // use default cache-first for refresh
});
}
$: if (job.state === 'running' && triggerMetricRefresh === true) {
refreshMetrics();
}
// Helper
const selectScope = (jobMetrics) =>
jobMetrics.reduce(
(a, b) =>
maxScope([a.scope, b.scope]) == a.scope
? job.numNodes > 1
? a
: b
: job.numNodes > 1
? b
: a,
jobMetrics[0],
);
const sortAndSelectScope = (jobMetrics) =>
metrics
.map((name) => jobMetrics.filter((jobMetric) => jobMetric.name == name))
.map((jobMetrics) => ({
disabled: false,
data: jobMetrics.length > 0 ? selectScope(jobMetrics) : null,
}))
.map((jobMetric) => {
if (jobMetric.data) {
return {
disabled: checkMetricDisabled(
jobMetric.data.name,
job.cluster,
job.subCluster,
),
data: jobMetric.data,
};
} else {
return jobMetric;
}
});
const sortOrder = (nodeMetrics) =>
selectedMetrics.map((name) => nodeMetrics.find((nodeMetric) => nodeMetric.name == name));
</script> </script>
<tr> <tr>
<td> <td>
<JobInfo {job} /> <NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} />
</td> </td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2} {#each sortOrder(nodeData?.data) as metricData}
<td colspan={metrics.length}>
<Card body color="warning">Not monitored or archiving failed</Card>
</td>
{:else if $metricsQuery.fetching}
<td colspan={metrics.length} style="text-align: center;">
<Spinner secondary />
</td>
{:else if $metricsQuery.error}
<td colspan={metrics.length}>
<Card body color="danger" class="mb-3">
{$metricsQuery.error.message.length > 500
? $metricsQuery.error.message.substring(0, 499) + "..."
: $metricsQuery.error.message}
</Card>
</td>
{:else}
{#if showFootprint}
<td> <td>
<JobFootprint {#if metricData}
{job} {#if nodeData?.disabled[metricData.name]}
width={plotWidth} <Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
height="{plotHeight}px"
displayTitle={false}
/>
</td>
{/if}
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)}
<td>
<!-- Subluster Metricconfig remove keyword for jobtables (joblist main, user joblist, project joblist) to be used here as toplevel case-->
{#if metric.disabled == false && metric.data}
<MetricPlot
on:zoom={({detail}) => { handleZoom(detail, metric.data.name) }}
height={plotHeight}
timestep={metric.data.metric.timestep}
scope={metric.data.scope}
series={metric.data.metric.series}
statisticsSeries={metric.data.metric.statisticsSeries}
metric={metric.data.name}
{cluster}
subCluster={job.subCluster}
isShared={job.exclusive != 1}
numhwthreads={job.numHWThreads}
numaccs={job.numAcc}
zoomState={zoomStates[metric.data.name] || null}
/>
{:else if metric.disabled == true && metric.data}
<Card body color="info"
>Metric disabled for subcluster <code >Metric disabled for subcluster <code
>{metric.data.name}:{job.subCluster}</code >{metricData.name}:{nodeData.subCluster}</code
></Card ></Card
> >
{:else} {:else}
<Card body color="warning">No dataset returned</Card> <MetricPlot
timestep={metricData.metric.timestep}
series={metricData.metric.series}
metric={metricData.name}
{cluster}
subCluster={nodeData.subCluster}
forNode={true}
/>
{/if}
{:else}
<Card
style="margin-left: 2rem;margin-right: 2rem;"
body
color="warning"
>No dataset returned for <code>{metricData.name}</code></Card
>
{/if} {/if}
</td> </td>
{/each} {/each}
{/if}
</tr> </tr>