mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-01-23 18:09:06 +01:00
add extended legend for nodelist acc metrics, move nodelist paging select
This commit is contained in:
parent
d0580592be
commit
735988decb
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `ìsAdmin Bool!`: Is currently logged in user admin authority
|
- `ìsAdmin Bool!`: Is currently logged in user admin authority
|
||||||
|
- `isSupport Bool!`: Is currently logged in user support authority
|
||||||
- `isApi Bool!`: Is currently logged in user api authority
|
- `isApi Bool!`: Is currently logged in user api authority
|
||||||
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
|
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
|
||||||
-->
|
-->
|
||||||
@ -10,15 +11,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
|
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
|
||||||
import UserSettings from "./config/UserSettings.svelte";
|
import UserSettings from "./config/UserSettings.svelte";
|
||||||
|
import SupportSettings from "./config/SupportSettings.svelte";
|
||||||
import AdminSettings from "./config/AdminSettings.svelte";
|
import AdminSettings from "./config/AdminSettings.svelte";
|
||||||
|
|
||||||
export let isAdmin;
|
export let isAdmin;
|
||||||
|
export let isSupport;
|
||||||
export let isApi;
|
export let isApi;
|
||||||
export let username;
|
export let username;
|
||||||
export let ncontent;
|
export let ncontent;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isAdmin == true}
|
{#if isAdmin}
|
||||||
<Card style="margin-bottom: 1.5em;">
|
<Card style="margin-bottom: 1.5em;">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-1">Admin Options</CardTitle>
|
<CardTitle class="mb-1">Admin Options</CardTitle>
|
||||||
@ -27,6 +30,15 @@
|
|||||||
</Card>
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if isSupport || isAdmin}
|
||||||
|
<Card style="margin-bottom: 1.5em;">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="mb-1">Support Options</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<SupportSettings/>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-1">User Options</CardTitle>
|
<CardTitle class="mb-1">User Options</CardTitle>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext, createEventDispatcher } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
@ -64,10 +64,8 @@
|
|||||||
let isMetricsSelectionOpen = false;
|
let isMetricsSelectionOpen = false;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Note 1: Scope Selector or Auto-Scoped? -> USeful auto scoping with stats view where applicable -> CHeck with JVe
|
Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
|
||||||
Note 2: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
|
Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
|
||||||
Note 3: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
|
|
||||||
Note 4: Resolution changes as implemented only possible for all plots generally, not for individual metrics: Result list if build from GQL result *including* metric series
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let systemMetrics = [];
|
let systemMetrics = [];
|
||||||
|
@ -5,6 +5,7 @@ new Config({
|
|||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
|
isSupport: isSupport,
|
||||||
isApi: isApi,
|
isApi: isApi,
|
||||||
username: username,
|
username: username,
|
||||||
ncontent: ncontent,
|
ncontent: ncontent,
|
||||||
|
13
web/frontend/src/config/SupportSettings.svelte
Normal file
13
web/frontend/src/config/SupportSettings.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!--
|
||||||
|
@component Support settings wrapper
|
||||||
|
Properties: None
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import SupportOptions from "./support/SupportOptions.svelte";
|
||||||
|
|
||||||
|
const ccconfig = getContext("cc-config");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SupportOptions config={ccconfig}/>
|
@ -4,13 +4,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte";
|
import { getContext, onMount } from "svelte";
|
||||||
import { Col, Card, CardBody, CardTitle, Button} from "@sveltestrap/sveltestrap";
|
import { Col, Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
|
|
||||||
export let config;
|
|
||||||
|
|
||||||
let message;
|
|
||||||
let displayMessage;
|
|
||||||
let scrambled;
|
let scrambled;
|
||||||
|
|
||||||
const resampleConfig = getContext("resampling");
|
const resampleConfig = getContext("resampling");
|
||||||
@ -28,33 +23,6 @@
|
|||||||
window.localStorage.removeItem("cc-scramble-names");
|
window.localStorage.removeItem("cc-scramble-names");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSettingSubmit(selector, target) {
|
|
||||||
let form = document.querySelector(selector);
|
|
||||||
let formData = new FormData(form);
|
|
||||||
try {
|
|
||||||
const res = await fetch(form.action, { method: "POST", body: formData });
|
|
||||||
if (res.ok) {
|
|
||||||
let text = await res.text();
|
|
||||||
popMessage(text, target, "#048109");
|
|
||||||
} else {
|
|
||||||
let text = await res.text();
|
|
||||||
throw new Error("Response Code " + res.status + "-> " + text);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
popMessage(err, target, "#d63384");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function popMessage(response, restarget, rescolor) {
|
|
||||||
message = { msg: response, target: restarget, color: rescolor };
|
|
||||||
displayMessage = true;
|
|
||||||
setTimeout(function () {
|
|
||||||
displayMessage = false;
|
|
||||||
}, 3500);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Col>
|
<Col>
|
||||||
@ -73,53 +41,6 @@
|
|||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col>
|
|
||||||
<Card class="h-100">
|
|
||||||
<form
|
|
||||||
id="node-paging-form"
|
|
||||||
method="post"
|
|
||||||
action="/frontend/configuration/"
|
|
||||||
class="card-body"
|
|
||||||
on:submit|preventDefault={() =>
|
|
||||||
handleSettingSubmit("#node-paging-form", "npag")}
|
|
||||||
>
|
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
|
||||||
<CardTitle
|
|
||||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
|
||||||
>
|
|
||||||
<div>Node List Paging Type</div>
|
|
||||||
{#if displayMessage && message.target == "npag"}<div
|
|
||||||
style="margin-left: auto; font-size: 0.9em;"
|
|
||||||
>
|
|
||||||
<code style="color: {message.color};" out:fade
|
|
||||||
>Update: {message.msg}</code
|
|
||||||
>
|
|
||||||
</div>{/if}
|
|
||||||
</CardTitle>
|
|
||||||
<input type="hidden" name="key" value="node_list_usePaging" />
|
|
||||||
<div class="mb-3">
|
|
||||||
<div>
|
|
||||||
{#if config?.node_list_usePaging}
|
|
||||||
<input type="radio" id="nodes-true-checked" name="value" value="true" checked />
|
|
||||||
{:else}
|
|
||||||
<input type="radio" id="nodes-true" name="value" value="true" />
|
|
||||||
{/if}
|
|
||||||
<label for="true">Paging with selectable count of nodes.</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{#if config?.node_list_usePaging}
|
|
||||||
<input type="radio" id="nodes-false" name="value" value="false" />
|
|
||||||
{:else}
|
|
||||||
<input type="radio" id="nodes-false-checked" name="value" value="false" checked />
|
|
||||||
{/if}
|
|
||||||
<label for="false">Continuous scroll iteratively adding 10 nodes.</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button color="primary" type="submit">Submit</Button>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{#if resampleConfig}
|
{#if resampleConfig}
|
||||||
<Col>
|
<Col>
|
||||||
<Card class="h-100">
|
<Card class="h-100">
|
||||||
|
89
web/frontend/src/config/support/SupportOptions.svelte
Normal file
89
web/frontend/src/config/support/SupportOptions.svelte
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!--
|
||||||
|
@component Support option select card
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Row, Col, Card, CardTitle, Button} from "@sveltestrap/sveltestrap";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
|
export let config;
|
||||||
|
|
||||||
|
let message;
|
||||||
|
let displayMessage;
|
||||||
|
|
||||||
|
async function handleSettingSubmit(selector, target) {
|
||||||
|
let form = document.querySelector(selector);
|
||||||
|
let formData = new FormData(form);
|
||||||
|
try {
|
||||||
|
const res = await fetch(form.action, { method: "POST", body: formData });
|
||||||
|
if (res.ok) {
|
||||||
|
let text = await res.text();
|
||||||
|
popMessage(text, target, "#048109");
|
||||||
|
} else {
|
||||||
|
let text = await res.text();
|
||||||
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
popMessage(err, target, "#d63384");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function popMessage(response, restarget, rescolor) {
|
||||||
|
message = { msg: response, target: restarget, color: rescolor };
|
||||||
|
displayMessage = true;
|
||||||
|
setTimeout(function () {
|
||||||
|
displayMessage = false;
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Row cols={1} class="p-2 g-2">
|
||||||
|
<Col>
|
||||||
|
<Card class="h-100">
|
||||||
|
<form
|
||||||
|
id="node-paging-form"
|
||||||
|
method="post"
|
||||||
|
action="/frontend/configuration/"
|
||||||
|
class="card-body"
|
||||||
|
on:submit|preventDefault={() =>
|
||||||
|
handleSettingSubmit("#node-paging-form", "npag")}
|
||||||
|
>
|
||||||
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
|
<CardTitle
|
||||||
|
style="margin-bottom: 1em; display: flex; align-items: center;"
|
||||||
|
>
|
||||||
|
<div>Node List Paging Type</div>
|
||||||
|
{#if displayMessage && message.target == "npag"}<div
|
||||||
|
style="margin-left: auto; font-size: 0.9em;"
|
||||||
|
>
|
||||||
|
<code style="color: {message.color};" out:fade
|
||||||
|
>Update: {message.msg}</code
|
||||||
|
>
|
||||||
|
</div>{/if}
|
||||||
|
</CardTitle>
|
||||||
|
<input type="hidden" name="key" value="node_list_usePaging" />
|
||||||
|
<div class="mb-3">
|
||||||
|
<div>
|
||||||
|
{#if config?.node_list_usePaging}
|
||||||
|
<input type="radio" id="nodes-true-checked" name="value" value="true" checked />
|
||||||
|
{:else}
|
||||||
|
<input type="radio" id="nodes-true" name="value" value="true" />
|
||||||
|
{/if}
|
||||||
|
<label for="true">Paging with selectable count of nodes.</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{#if config?.node_list_usePaging}
|
||||||
|
<input type="radio" id="nodes-false" name="value" value="false" />
|
||||||
|
{:else}
|
||||||
|
<input type="radio" id="nodes-false-checked" name="value" value="false" checked />
|
||||||
|
{/if}
|
||||||
|
<label for="false">Continuous scroll iteratively adding 10 nodes.</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button color="primary" type="submit">Submit</Button>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
@ -138,6 +138,7 @@
|
|||||||
export let numaccs = 0;
|
export let numaccs = 0;
|
||||||
export let zoomState = null;
|
export let zoomState = null;
|
||||||
export let thresholdState = null;
|
export let thresholdState = null;
|
||||||
|
export let extendedLegendData = null;
|
||||||
|
|
||||||
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
|
if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true;
|
||||||
|
|
||||||
@ -191,6 +192,7 @@
|
|||||||
className && legendEl.classList.add(className);
|
className && legendEl.classList.add(className);
|
||||||
|
|
||||||
uPlot.assign(legendEl.style, {
|
uPlot.assign(legendEl.style, {
|
||||||
|
minWidth: extendedLegendData ? "300px" : "100px",
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
display: "none",
|
display: "none",
|
||||||
@ -307,13 +309,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const plotSeries = [
|
|
||||||
{
|
|
||||||
label: "Runtime",
|
|
||||||
value: (u, ts, sidx, didx) =>
|
|
||||||
didx == null ? null : formatTime(ts, forNode),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const plotData = [new Array(longestSeries)];
|
const plotData = [new Array(longestSeries)];
|
||||||
if (forNode === true) {
|
if (forNode === true) {
|
||||||
// Negative Timestamp Buildup
|
// Negative Timestamp Buildup
|
||||||
@ -330,6 +325,15 @@
|
|||||||
plotData[0][j] = j * timestep;
|
plotData[0][j] = j * timestep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const plotSeries = [
|
||||||
|
// Note: X-Legend Will not be shown as soon as Y-Axis are in extendedMode
|
||||||
|
{
|
||||||
|
label: "Runtime",
|
||||||
|
value: (u, ts, sidx, didx) =>
|
||||||
|
(didx == null) ? null : formatTime(ts, forNode),
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
let plotBands = undefined;
|
let plotBands = undefined;
|
||||||
if (useStatsSeries) {
|
if (useStatsSeries) {
|
||||||
plotData.push(statisticsSeries.min);
|
plotData.push(statisticsSeries.min);
|
||||||
@ -366,16 +370,61 @@
|
|||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < series.length; i++) {
|
for (let i = 0; i < series.length; i++) {
|
||||||
plotData.push(series[i].data);
|
plotData.push(series[i].data);
|
||||||
|
// Default
|
||||||
|
if (!extendedLegendData) {
|
||||||
plotSeries.push({
|
plotSeries.push({
|
||||||
label:
|
label:
|
||||||
scope === "node"
|
scope === "node"
|
||||||
? series[i].hostname
|
? series[i].hostname
|
||||||
|
: scope === "accelerator"
|
||||||
|
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
|
||||||
: scope + " #" + (i + 1),
|
: scope + " #" + (i + 1),
|
||||||
scale: "y",
|
scale: "y",
|
||||||
width: lineWidth,
|
width: lineWidth,
|
||||||
stroke: lineColor(i, series.length),
|
stroke: lineColor(i, series.length),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Extended Legend For NodeList
|
||||||
|
else {
|
||||||
|
plotSeries.push({
|
||||||
|
label:
|
||||||
|
scope === "node"
|
||||||
|
? series[i].hostname
|
||||||
|
: scope === "accelerator"
|
||||||
|
? 'Acc #' + (i + 1) // series[i].id.slice(9, 14) | Too Hardware Specific
|
||||||
|
: scope + " #" + (i + 1),
|
||||||
|
scale: "y",
|
||||||
|
width: lineWidth,
|
||||||
|
stroke: lineColor(i, series.length),
|
||||||
|
values: (u, sidx, idx) => {
|
||||||
|
// "i" = "sidx - 1" : sidx contains x-axis-data
|
||||||
|
if (idx == null)
|
||||||
|
return {
|
||||||
|
time: '-',
|
||||||
|
value: '-',
|
||||||
|
user: '-',
|
||||||
|
job: '-'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (series[i].id in extendedLegendData) {
|
||||||
|
return {
|
||||||
|
time: formatTime(plotData[0][idx], forNode),
|
||||||
|
value: plotData[sidx][idx],
|
||||||
|
user: extendedLegendData[series[i].id].user,
|
||||||
|
job: extendedLegendData[series[i].id].job,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
time: formatTime(plotData[0][idx], forNode),
|
||||||
|
value: plotData[sidx][idx],
|
||||||
|
user: '-',
|
||||||
|
job: '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
Spinner,
|
|
||||||
Icon,
|
Icon,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@ -18,61 +17,24 @@
|
|||||||
Input,
|
Input,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupText, } from "@sveltestrap/sveltestrap";
|
InputGroupText, } from "@sveltestrap/sveltestrap";
|
||||||
import {
|
|
||||||
queryStore,
|
|
||||||
gql,
|
|
||||||
getContextClient,
|
|
||||||
} from "@urql/svelte";
|
|
||||||
|
|
||||||
export let cluster;
|
export let cluster;
|
||||||
export let subCluster
|
export let subCluster
|
||||||
export let hostname;
|
export let hostname;
|
||||||
export let dataHealth;
|
export let dataHealth;
|
||||||
|
export let nodeJobsData = null;
|
||||||
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"] },
|
|
||||||
];
|
|
||||||
|
|
||||||
const nodeJobsQuery = gql`
|
|
||||||
query (
|
|
||||||
$filter: [JobFilter!]!
|
|
||||||
$sorting: OrderByInput!
|
|
||||||
$paging: PageRequest!
|
|
||||||
) {
|
|
||||||
jobs(filter: $filter, order: $sorting, page: $paging) {
|
|
||||||
items {
|
|
||||||
user
|
|
||||||
project
|
|
||||||
exclusive
|
|
||||||
}
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Not at least one returned, selected metric: NodeHealth warning
|
// Not at least one returned, selected metric: NodeHealth warning
|
||||||
const healthWarn = !dataHealth.includes(true);
|
const healthWarn = !dataHealth.includes(true);
|
||||||
// At least one non-returned selected metric: Metric config error?
|
// At least one non-returned selected metric: Metric config error?
|
||||||
const metricWarn = dataHealth.includes(false);
|
const metricWarn = dataHealth.includes(false);
|
||||||
|
|
||||||
$: nodeJobsData = queryStore({
|
|
||||||
client: client,
|
|
||||||
query: nodeJobsQuery,
|
|
||||||
variables: { paging, sorting, filter },
|
|
||||||
});
|
|
||||||
|
|
||||||
let userList;
|
let userList;
|
||||||
let projectList;
|
let projectList;
|
||||||
$: if ($nodeJobsData?.data) {
|
$: if (nodeJobsData) {
|
||||||
userList = Array.from(new Set($nodeJobsData.data.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b));
|
userList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b));
|
||||||
projectList = Array.from(new Set($nodeJobsData.data.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b));
|
projectList = Array.from(new Set(nodeJobsData.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card class="pb-3">
|
<Card class="pb-3">
|
||||||
@ -92,9 +54,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{#if $nodeJobsData.fetching}
|
|
||||||
<Spinner />
|
|
||||||
{:else if $nodeJobsData.data}
|
|
||||||
{#if healthWarn}
|
{#if healthWarn}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupText>
|
<InputGroupText>
|
||||||
@ -119,7 +78,7 @@
|
|||||||
Missing Metric
|
Missing Metric
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{:else if $nodeJobsData.data.jobs.count == 1 && $nodeJobsData.data.jobs.items[0].exclusive}
|
{:else if nodeJobsData.jobs.count == 1 && nodeJobsData.jobs.items[0].exclusive}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupText>
|
<InputGroupText>
|
||||||
<Icon name="circle-fill"/>
|
<Icon name="circle-fill"/>
|
||||||
@ -131,7 +90,7 @@
|
|||||||
Exclusive
|
Exclusive
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{:else if $nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive}
|
{:else if nodeJobsData.jobs.count >= 1 && !nodeJobsData.jobs.items[0].exclusive}
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupText>
|
<InputGroupText>
|
||||||
<Icon name="circle-half"/>
|
<Icon name="circle-half"/>
|
||||||
@ -165,7 +124,7 @@
|
|||||||
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
|
<InputGroupText class="justify-content-center" style="width: 4.4rem;">
|
||||||
Activity
|
Activity
|
||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{$nodeJobsData?.data?.jobs?.count || 0} Job{($nodeJobsData?.data?.jobs?.count == 1) ? '': 's'}" disabled />
|
<Input class="flex-grow-1" style="background-color: white;" type="text" value="{nodeJobsData?.jobs?.count || 0} Job{(nodeJobsData?.jobs?.count == 1) ? '': 's'}" 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" >
|
<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" />
|
<Icon name="view-list" />
|
||||||
List
|
List
|
||||||
@ -213,7 +172,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
@ -8,7 +8,12 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Card } from "@sveltestrap/sveltestrap";
|
import {
|
||||||
|
queryStore,
|
||||||
|
gql,
|
||||||
|
getContextClient,
|
||||||
|
} from "@urql/svelte";
|
||||||
|
import { Card, CardBody, Spinner } from "@sveltestrap/sveltestrap";
|
||||||
import { maxScope, checkMetricDisabled } from "../../generic/utils.js";
|
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";
|
||||||
@ -17,6 +22,43 @@
|
|||||||
export let nodeData;
|
export let nodeData;
|
||||||
export let selectedMetrics;
|
export let selectedMetrics;
|
||||||
|
|
||||||
|
const client = getContextClient();
|
||||||
|
const paging = { itemsPerPage: 50, page: 1 };
|
||||||
|
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||||
|
const filter = [
|
||||||
|
{ cluster: { eq: cluster } },
|
||||||
|
{ node: { contains: nodeData.host } },
|
||||||
|
{ state: ["running"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const nodeJobsQuery = gql`
|
||||||
|
query (
|
||||||
|
$filter: [JobFilter!]!
|
||||||
|
$sorting: OrderByInput!
|
||||||
|
$paging: PageRequest!
|
||||||
|
) {
|
||||||
|
jobs(filter: $filter, order: $sorting, page: $paging) {
|
||||||
|
items {
|
||||||
|
jobId
|
||||||
|
user
|
||||||
|
project
|
||||||
|
exclusive
|
||||||
|
resources {
|
||||||
|
hostname
|
||||||
|
accelerators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
$: nodeJobsData = queryStore({
|
||||||
|
client: client,
|
||||||
|
query: nodeJobsQuery,
|
||||||
|
variables: { paging, sorting, filter },
|
||||||
|
});
|
||||||
|
|
||||||
// Helper
|
// Helper
|
||||||
const selectScope = (nodeMetrics) =>
|
const selectScope = (nodeMetrics) =>
|
||||||
nodeMetrics.reduce(
|
nodeMetrics.reduce(
|
||||||
@ -51,14 +93,44 @@
|
|||||||
let dataHealth;
|
let dataHealth;
|
||||||
$: if (nodeData?.metrics) {
|
$: if (nodeData?.metrics) {
|
||||||
refinedData = sortAndSelectScope(nodeData?.metrics)
|
refinedData = sortAndSelectScope(nodeData?.metrics)
|
||||||
// Check data for series, skip disabled
|
|
||||||
dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0))
|
dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let extendedLegendData = null;
|
||||||
|
$: if ($nodeJobsData?.data) {
|
||||||
|
// Get Shared State of Node: Only Build extended Legend For Shared Nodes
|
||||||
|
if ($nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive) {
|
||||||
|
const accSet = Array.from(new Set($nodeJobsData.data.jobs.items
|
||||||
|
.map((i) => i.resources
|
||||||
|
.filter((r) => r.hostname === nodeData.host)
|
||||||
|
.map((r) => r.accelerators)
|
||||||
|
)
|
||||||
|
)).flat(2)
|
||||||
|
|
||||||
|
extendedLegendData = {}
|
||||||
|
for (const accId of accSet) {
|
||||||
|
const matchJob = $nodeJobsData.data.jobs.items.find((i) => i.resources.find((r) => r.accelerators.includes(accId)))
|
||||||
|
extendedLegendData[accId] = {
|
||||||
|
user: matchJob?.user ? matchJob?.user : '-',
|
||||||
|
job: matchJob?.jobId ? matchJob?.jobId : '-',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Theoretically extendable for hwthreadIDs
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<NodeInfo {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} {dataHealth}/>
|
{#if $nodeJobsData.fetching}
|
||||||
|
<Card>
|
||||||
|
<CardBody class="content-center">
|
||||||
|
<Spinner/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
{:else}
|
||||||
|
<NodeInfo nodeJobsData={$nodeJobsData.data} {cluster} subCluster={nodeData.subCluster} hostname={nodeData.host} {dataHealth}/>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{#each refinedData as metricData (metricData.data.name)}
|
{#each refinedData as metricData (metricData.data.name)}
|
||||||
<td>
|
<td>
|
||||||
@ -83,6 +155,7 @@
|
|||||||
forNode
|
forNode
|
||||||
/>
|
/>
|
||||||
<div class="my-2"/>
|
<div class="my-2"/>
|
||||||
|
{#key extendedLegendData}
|
||||||
<MetricPlot
|
<MetricPlot
|
||||||
{cluster}
|
{cluster}
|
||||||
subCluster={nodeData.subCluster}
|
subCluster={nodeData.subCluster}
|
||||||
@ -91,8 +164,10 @@
|
|||||||
timestep={metricData.data.metric.timestep}
|
timestep={metricData.data.metric.timestep}
|
||||||
series={metricData.data.metric.series}
|
series={metricData.data.metric.series}
|
||||||
height={175}
|
height={175}
|
||||||
|
{extendedLegendData}
|
||||||
forNode
|
forNode
|
||||||
/>
|
/>
|
||||||
|
{/key}
|
||||||
{:else}
|
{:else}
|
||||||
<MetricPlot
|
<MetricPlot
|
||||||
{cluster}
|
{cluster}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
{{define "javascript"}}
|
{{define "javascript"}}
|
||||||
<script>
|
<script>
|
||||||
const isAdmin = {{ .User.HasRole .Roles.admin }};
|
const isAdmin = {{ .User.HasRole .Roles.admin }};
|
||||||
|
const isSupport = {{ .User.HasRole .Roles.support }};
|
||||||
const isApi = {{ .User.HasRole .Roles.api }};
|
const isApi = {{ .User.HasRole .Roles.api }};
|
||||||
const username = {{ .User.Username }};
|
const username = {{ .User.Username }};
|
||||||
const filterPresets = {{ .FilterPresets }};
|
const filterPresets = {{ .FilterPresets }};
|
||||||
|
Loading…
Reference in New Issue
Block a user