mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-01-24 02:19:05 +01:00
Finish prototype implementation of nodelist view
This commit is contained in:
parent
2f6e5a7648
commit
673fdc443c
@ -44,7 +44,7 @@
|
||||
if (from == null || to == null) {
|
||||
to = new Date(Date.now());
|
||||
from = new Date(to.getTime());
|
||||
from.setHours(from.getHours() - 12);
|
||||
from.setHours(from.getHours() - 4);
|
||||
}
|
||||
|
||||
const initialized = getContext("initialized")
|
||||
@ -156,15 +156,17 @@
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="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">
|
||||
<Icon name="view-list" /> Show List
|
||||
</a>
|
||||
</InputGroup>
|
||||
{:else}
|
||||
<Input type="text" disabled>
|
||||
No currently running jobs.
|
||||
</Input>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="activity" /></InputGroupText>
|
||||
<InputGroupText>Activity</InputGroupText>
|
||||
<Input type="text" value="No running jobs." disabled />
|
||||
</InputGroup>
|
||||
{/if}
|
||||
</Col>
|
||||
<!-- Refresh Col-->
|
||||
|
@ -45,7 +45,7 @@
|
||||
if (from == null || to == null) {
|
||||
to = new Date(Date.now());
|
||||
from = new Date(to.getTime());
|
||||
from.setHours(from.getHours() - 12);
|
||||
from.setHours(from.getHours() - 4);
|
||||
}
|
||||
|
||||
const initialized = getContext("initialized");
|
||||
@ -53,12 +53,15 @@
|
||||
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 selectedMetrics = ccconfig[`node_list_selectedMetrics:${cluster}`] || [ccconfig.system_view_selectedMetric];
|
||||
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 nodeQuery = gql`
|
||||
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
|
||||
@ -139,7 +142,7 @@
|
||||
mappedData = rawData.map((h) => ({
|
||||
host: h.host,
|
||||
subCluster: h.subCluster,
|
||||
data: h.metrics.find(
|
||||
data: h.metrics.filter(
|
||||
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
|
||||
),
|
||||
disabled: checkMetricsDisabled(
|
||||
@ -157,24 +160,23 @@
|
||||
h.host.includes(hostnameFilter)
|
||||
)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- ROW1: Tools-->
|
||||
<Row cols={{ xs: 2, lg: 4 }}>
|
||||
<Row cols={{ xs: 2, lg: 4 }} class="mb-3">
|
||||
{#if $initq.data}
|
||||
<!-- List Metric Select Col-->
|
||||
{#if !displayNodeOverview}
|
||||
<Col>
|
||||
<InputGroup>
|
||||
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
||||
<InputGroupText>Metrics</InputGroupText>
|
||||
<InputGroupText class="text-capitalize">Metrics</InputGroupText>
|
||||
<Button
|
||||
outline
|
||||
color="secondary"
|
||||
color="primary"
|
||||
on:click={() => (isMetricsSelectionOpen = true)}
|
||||
>
|
||||
Select for {cluster} ...
|
||||
{selectedMetrics.length} selected
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
@ -187,7 +189,6 @@
|
||||
<Input
|
||||
placeholder="Filter hostname ..."
|
||||
type="text"
|
||||
title="Search with at least three characters ..."
|
||||
bind:value={hostnameFilter}
|
||||
/>
|
||||
</InputGroup>
|
||||
@ -247,21 +248,19 @@
|
||||
{:else if $initialized && $nodesQuery?.data}
|
||||
{#if displayNodeOverview}
|
||||
<!-- ROW2-1: Node Overview (Grid Included)-->
|
||||
<NodeOverview {ccconfig} {cluster} bind:selectedMetric data={filteredData}/>
|
||||
|
||||
<NodeOverview {cluster} {ccconfig} data={filteredData} bind:selectedMetric/>
|
||||
{: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}
|
||||
/>
|
||||
<!-- ROW2-2: Node List (Grid Included)-->
|
||||
<NodeList {cluster} {selectedMetrics} {systemUnits} data={filteredData}/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<MetricSelection
|
||||
{cluster}
|
||||
configName="node_list_selectedMetrics"
|
||||
metrics={selectedMetrics}
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
on:update-metrics={({ detail }) => {
|
||||
selectedMetrics = [...detail]
|
||||
}}
|
||||
/>
|
||||
|
@ -12,7 +12,7 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import { getContext, createEventDispatcher } from "svelte";
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
@ -33,6 +33,7 @@
|
||||
|
||||
const onInit = getContext("on-init")
|
||||
const globalMetrics = getContext("globalMetrics")
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let newMetricsOrder = [];
|
||||
let unorderedMetrics = [...metrics];
|
||||
@ -128,6 +129,8 @@
|
||||
throw res.error;
|
||||
}
|
||||
});
|
||||
|
||||
dispatch('update-metrics', metrics);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -5,86 +5,22 @@
|
||||
- `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:
|
||||
- `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>
|
||||
import {
|
||||
gql,
|
||||
mutationStore,
|
||||
} from "@urql/svelte";
|
||||
import { Row, Table } from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
checkMetricsDisabled,
|
||||
stickyHeader
|
||||
} from "../generic/utils.js";
|
||||
import NodeListRow from "./nodelist/NodeListRow.svelte";
|
||||
|
||||
export let cluster;
|
||||
export let nodesData = null;
|
||||
export let data = null;
|
||||
export let selectedMetrics = [];
|
||||
export let systemUnits = null;
|
||||
export let hostnameFilter = "";
|
||||
|
||||
// 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;
|
||||
stickyHeader(
|
||||
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
|
||||
@ -98,11 +34,11 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="position-sticky top-0"
|
||||
class="position-sticky top-0 text-capitalize"
|
||||
scope="col"
|
||||
style="padding-top: {headerPaddingTop}px"
|
||||
>
|
||||
Node Info
|
||||
{cluster} Node Info
|
||||
</th>
|
||||
|
||||
{#each selectedMetrics as metric (metric)}
|
||||
@ -117,9 +53,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each nodes as node (node)}
|
||||
{node}
|
||||
<!-- <NodeListRow {node} {selectedMetrics} /> -->
|
||||
{#each data as nodeData (nodeData)}
|
||||
<NodeListRow {nodeData} {cluster} {selectedMetrics}/>
|
||||
{:else}
|
||||
<tr>
|
||||
<td>No nodes found </td>
|
||||
|
@ -35,7 +35,7 @@
|
||||
>{item.host} ({item.subCluster})</a
|
||||
>
|
||||
</h4>
|
||||
{#if item?.data}
|
||||
{#if item?.data[0]}
|
||||
{#if item?.disabled[selectedMetric]}
|
||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
|
||||
>Metric disabled for subcluster <code
|
||||
@ -44,9 +44,9 @@
|
||||
>
|
||||
{:else}
|
||||
<MetricPlot
|
||||
timestep={item.data.metric.timestep}
|
||||
series={item.data.metric.series}
|
||||
metric={item.data.name}
|
||||
timestep={item.data[0].metric.timestep}
|
||||
series={item.data[0].metric.series}
|
||||
metric={item.data[0].name}
|
||||
cluster={clusters.find((c) => c.name == cluster)}
|
||||
subCluster={item.subCluster}
|
||||
forNode={true}
|
||||
|
@ -7,146 +7,159 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Badge, Icon } from "@sveltestrap/sveltestrap";
|
||||
import { scrambleNames, scramble } from "../../generic/utils.js";
|
||||
import Tag from "../../generic/helper/Tag.svelte";
|
||||
import TagManagement from "../../generic/helper/TagManagement.svelte";
|
||||
import {
|
||||
Spinner,
|
||||
Icon,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText, } from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
queryStore,
|
||||
gql,
|
||||
getContextClient,
|
||||
} from "@urql/svelte";
|
||||
|
||||
export let job;
|
||||
export let jobTags = job.tags;
|
||||
export let showTagedit = false;
|
||||
export let username = null;
|
||||
export let authlevel= null;
|
||||
export let roles = null;
|
||||
export let cluster;
|
||||
export let subCluster
|
||||
export let hostname;
|
||||
|
||||
function formatDuration(duration) {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
duration -= hours * 3600;
|
||||
const minutes = Math.floor(duration / 60);
|
||||
duration -= minutes * 60;
|
||||
const seconds = duration;
|
||||
return `${hours}:${("0" + minutes).slice(-2)}:${("0" + seconds).slice(-2)}`;
|
||||
}
|
||||
const client = getContextClient();
|
||||
const paging = { itemsPerPage: 50, page: 1 };
|
||||
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||
const filter = [
|
||||
{ cluster: { eq: cluster } },
|
||||
{ node: { contains: hostname } },
|
||||
{ state: ["running"] },
|
||||
];
|
||||
|
||||
function getStateColor(state) {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return "success";
|
||||
case "completed":
|
||||
return "primary";
|
||||
default:
|
||||
return "danger";
|
||||
const nodeJobsQuery = gql`
|
||||
query (
|
||||
$filter: [JobFilter!]!
|
||||
$sorting: OrderByInput!
|
||||
$paging: PageRequest!
|
||||
) {
|
||||
jobs(filter: $filter, order: $sorting, page: $paging) {
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
$: nodeJobsData = queryStore({
|
||||
client: client,
|
||||
query: nodeJobsQuery,
|
||||
variables: { paging, sorting, filter },
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p class="mb-2">
|
||||
<span class="fw-bold"
|
||||
><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
||||
({job.cluster})</span
|
||||
>
|
||||
{#if job.metaData?.jobName}
|
||||
<br />
|
||||
{#if job.metaData?.jobName.length <= 25}
|
||||
<div>{job.metaData.jobName}</div>
|
||||
{:else}
|
||||
<div
|
||||
class="truncate"
|
||||
style="cursor:help; width:230px;"
|
||||
title={job.metaData.jobName}
|
||||
>
|
||||
{job.metaData.jobName}
|
||||
<Card class="pb-2">
|
||||
<CardHeader class="d-inline-flex justify-content-between align-items-end">
|
||||
<div>
|
||||
<h5 class="mb-0">
|
||||
Node
|
||||
<a href="/monitoring/node/{cluster}/{hostname}" target="_blank">
|
||||
{hostname}
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if job.arrayJobId}
|
||||
Array Job: <a
|
||||
href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}"
|
||||
target="_blank">#{job.arrayJobId}</a
|
||||
>
|
||||
<div class="text-capitalize">
|
||||
<h6 class="mb-0">
|
||||
{cluster} {subCluster}
|
||||
</h6>
|
||||
</div>
|
||||
</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}
|
||||
</p>
|
||||
|
||||
<p class="mb-2">
|
||||
<Icon name="person-fill" />
|
||||
<a class="fst-italic" href="/monitoring/user/{job.user}" target="_blank">
|
||||
{scrambleNames ? scramble(job.user) : job.user}
|
||||
<hr class="mt-0 mb-3"/>
|
||||
<p>
|
||||
{#if $nodeJobsData.data.jobs.count > 0}
|
||||
<InputGroup class="justify-content-between">
|
||||
<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>
|
||||
{#if job.userData && job.userData.name}
|
||||
({scrambleNames ? scramble(job.userData.name) : job.userData.name})
|
||||
</InputGroup>
|
||||
{: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 job.project && job.project != "no project"}
|
||||
<br />
|
||||
<Icon name="people-fill" />
|
||||
<a
|
||||
class="fst-italic"
|
||||
href="/monitoring/jobs/?project={job.project}&projectMatch=eq"
|
||||
target="_blank"
|
||||
>
|
||||
{scrambleNames ? scramble(job.project) : job.project}
|
||||
</p>
|
||||
<p>
|
||||
<InputGroup class="justify-content-between">
|
||||
<InputGroupText>
|
||||
<Icon name="people"/>
|
||||
</InputGroupText>
|
||||
<InputGroupText class="flex-fill">
|
||||
Users on Node
|
||||
</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>
|
||||
{/if}
|
||||
</InputGroup>
|
||||
</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>
|
||||
|
@ -5,203 +5,52 @@
|
||||
- `job Object`: The job object (GraphQL.Job)
|
||||
- `metrics [String]`: Currently selected metrics
|
||||
- `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>
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import { getContext } from "svelte";
|
||||
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||
import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
|
||||
import { Card } from "@sveltestrap/sveltestrap";
|
||||
import MetricPlot from "../../generic/plots/MetricPlot.svelte";
|
||||
|
||||
import NodeInfo from "./NodeInfo.svelte";
|
||||
|
||||
export let job;
|
||||
export let metrics;
|
||||
export let plotWidth;
|
||||
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;
|
||||
}
|
||||
});
|
||||
export let cluster;
|
||||
export let nodeData;
|
||||
export let selectedMetrics;
|
||||
|
||||
const sortOrder = (nodeMetrics) =>
|
||||
selectedMetrics.map((name) => nodeMetrics.find((nodeMetric) => nodeMetric.name == name));
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<JobInfo {job} />
|
||||
<NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} />
|
||||
</td>
|
||||
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
|
||||
<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}
|
||||
{#each sortOrder(nodeData?.data) as metricData}
|
||||
<td>
|
||||
<JobFootprint
|
||||
{job}
|
||||
width={plotWidth}
|
||||
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"
|
||||
{#if metricData}
|
||||
{#if nodeData?.disabled[metricData.name]}
|
||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
|
||||
>Metric disabled for subcluster <code
|
||||
>{metric.data.name}:{job.subCluster}</code
|
||||
>{metricData.name}:{nodeData.subCluster}</code
|
||||
></Card
|
||||
>
|
||||
{: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}
|
||||
</td>
|
||||
{/each}
|
||||
{/if}
|
||||
</tr>
|
||||
|
Loading…
Reference in New Issue
Block a user