mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-01-24 10:29:06 +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) {
|
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-->
|
||||||
|
@ -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]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user