mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-01-24 10:29:06 +01:00
Move common logic into systems view again
- adds backend log if subcluster for node not configured
This commit is contained in:
parent
2cbe8e9517
commit
2f6e5a7648
@ -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 {
|
||||||
|
@ -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}
|
||||||
<Row>
|
<Col>
|
||||||
<Col>
|
<InputGroup>
|
||||||
<Card color="danger">
|
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
||||||
Unknown displayList type!
|
<InputGroupText>Metrics</InputGroupText>
|
||||||
</Card>
|
<Button
|
||||||
</Col>
|
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>
|
</Row>
|
||||||
|
|
||||||
|
<!-- ROW2: Content-->
|
||||||
|
{#if displayType !== "OVERVIEW" && displayType !== "LIST"}
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Card body color="danger">Unknown displayList type! </Card>
|
||||||
|
</Col>
|
||||||
|
</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}
|
||||||
|
@ -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 })
|
||||||
|
@ -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)";
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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}
|
{:else}
|
||||||
{#each jobs as job (job)}
|
|
||||||
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} />
|
|
||||||
{:else}
|
|
||||||
<tr>
|
|
||||||
<td colspan={metrics.length + 1}> No jobs found </td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{#if $jobsStore.fetching || !$jobsStore.data}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan={metrics.length + 1}>
|
<td>No nodes found </td>
|
||||||
<div style="text-align:center;">
|
|
||||||
<Spinner secondary />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/each}
|
||||||
</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;
|
||||||
|
@ -2,210 +2,47 @@
|
|||||||
@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 }}>
|
<PlotGrid
|
||||||
{#if $initq.error}
|
let:item
|
||||||
<Card body color="danger">{$initq.error.message}</Card>
|
renderFor="systems"
|
||||||
{:else if $initq.fetching}
|
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
||||||
<Spinner />
|
items={data}
|
||||||
{:else}
|
>
|
||||||
<!-- Node Col-->
|
<h4 style="width: 100%; text-align: center;">
|
||||||
<Col>
|
<a
|
||||||
<InputGroup>
|
style="display: block;padding-top: 15px;"
|
||||||
<InputGroupText><Icon name="hdd" /></InputGroupText>
|
href="/monitoring/node/{cluster}/{item.host}"
|
||||||
<InputGroupText>Find Node</InputGroupText>
|
>{item.host} ({item.subCluster})</a
|
||||||
<Input
|
>
|
||||||
placeholder="hostname..."
|
</h4>
|
||||||
type="text"
|
{#if item?.data}
|
||||||
bind:value={hostnameFilter}
|
{#if item?.disabled[selectedMetric]}
|
||||||
/>
|
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
|
||||||
</InputGroup>
|
>Metric disabled for subcluster <code
|
||||||
</Col>
|
>{selectedMetric}:{item.subCluster}</code
|
||||||
<!-- Range Col-->
|
></Card
|
||||||
<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
|
|
||||||
let:item
|
|
||||||
renderFor="systems"
|
|
||||||
itemsPerRow={ccconfig.plot_view_plotsPerRow}
|
|
||||||
items={$nodesQuery.data.nodeMetrics
|
|
||||||
.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;">
|
|
||||||
<a
|
|
||||||
style="display: block;padding-top: 15px;"
|
|
||||||
href="/monitoring/node/{cluster}/{item.host}"
|
|
||||||
>{item.host} ({item.subCluster})</a
|
|
||||||
>
|
>
|
||||||
</h4>
|
{:else}
|
||||||
{#if item.disabled === false && item.data}
|
|
||||||
<MetricPlot
|
<MetricPlot
|
||||||
timestep={item.data.metric.timestep}
|
timestep={item.data.metric.timestep}
|
||||||
series={item.data.metric.series}
|
series={item.data.metric.series}
|
||||||
@ -214,19 +51,13 @@
|
|||||||
subCluster={item.subCluster}
|
subCluster={item.subCluster}
|
||||||
forNode={true}
|
forNode={true}
|
||||||
/>
|
/>
|
||||||
{:else if item.disabled === true && item.data}
|
|
||||||
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info"
|
|
||||||
>Metric disabled for subcluster <code
|
|
||||||
>{selectedMetric}:{item.subCluster}</code
|
|
||||||
></Card
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<Card
|
|
||||||
style="margin-left: 2rem;margin-right: 2rem;"
|
|
||||||
body
|
|
||||||
color="warning"
|
|
||||||
>No dataset returned for <code>{selectedMetric}</code></Card
|
|
||||||
>
|
|
||||||
{/if}
|
{/if}
|
||||||
</PlotGrid>
|
{:else}
|
||||||
{/if}
|
<Card
|
||||||
|
style="margin-left: 2rem;margin-right: 2rem;"
|
||||||
|
body
|
||||||
|
color="warning"
|
||||||
|
>No dataset returned for <code>{selectedMetric}</code></Card
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</PlotGrid>
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user