mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-01 11:13:50 +02:00
232 lines
6.9 KiB
Svelte
232 lines
6.9 KiB
Svelte
<!--
|
|
@component Data row for a single job displaying metric plots
|
|
|
|
Properties:
|
|
- `job Object`: The job object (GraphQL.Job)
|
|
- `metrics [String]`: Currently selected metrics
|
|
- `plotWidth Number`: Width of the sub-components
|
|
- `plotHeight Number?`: Height of the sub-components [Default: 275]
|
|
- `showFootprint Bool`: Display of footprint component for job
|
|
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query
|
|
-->
|
|
|
|
<script>
|
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
|
import { getContext } from "svelte";
|
|
import { Card, Spinner } from "@sveltestrap/sveltestrap";
|
|
import { maxScope, checkMetricDisabled } from "../utils.js";
|
|
import JobInfo from "./JobInfo.svelte";
|
|
import MetricPlot from "../plots/MetricPlot.svelte";
|
|
import JobFootprint from "../helper/JobFootprint.svelte";
|
|
|
|
/* Svelte 5 Props */
|
|
let {
|
|
triggerMetricRefresh = false,
|
|
job,
|
|
metrics,
|
|
plotWidth,
|
|
plotHeight = 275,
|
|
showFootprint,
|
|
previousSelect = false,
|
|
selectJob,
|
|
unselectJob
|
|
} = $props();
|
|
|
|
/* Const Init */
|
|
const client = getContextClient();
|
|
const jobId = job.id;
|
|
const cluster = getContext("clusters");
|
|
const scopes = (job.numNodes == 1)
|
|
? (job.numAcc >= 1)
|
|
? ["core", "accelerator"]
|
|
: ["core"]
|
|
: ["node"];
|
|
const resampleConfig = getContext("resampling") || null;
|
|
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
/* State Init */
|
|
let selectedResolution = $state(resampleDefault);
|
|
let zoomStates = $state({});
|
|
let thresholdStates = $state({});
|
|
|
|
/* Derived */
|
|
let isSelected = $derived(previousSelect);
|
|
let metricsQuery = $derived(queryStore({
|
|
client: client,
|
|
query: query,
|
|
variables: { id: jobId, metrics, scopes, selectedResolution },
|
|
})
|
|
);
|
|
|
|
/* Effects */
|
|
$effect(() => {
|
|
if (job.state === 'running' && triggerMetricRefresh === true) {
|
|
refreshMetrics();
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (isSelected == true && previousSelect == false) {
|
|
selectJob(jobId)
|
|
} else if (isSelected == false && previousSelect == true) {
|
|
unselectJob(jobId)
|
|
}
|
|
});
|
|
|
|
/* Functions */
|
|
function handleZoom(detail, metric) {
|
|
// Buffer last zoom state to allow seamless zoom on rerender
|
|
// console.log('Update zoomState for/with:', metric, {...detail.lastZoomState})
|
|
zoomStates[metric] = detail?.lastZoomState ? {...detail.lastZoomState} : null;
|
|
// Handle to correctly reset on summed metric scope change
|
|
// console.log('Update thresholdState for/with:', metric, detail.lastThreshold)
|
|
thresholdStates[metric] = detail?.lastThreshold ? detail.lastThreshold : null;
|
|
// Triggers GQL
|
|
if (detail?.newRes) {
|
|
// console.log('Update selectedResolution for/with:', metric, detail.newRes)
|
|
selectedResolution = detail.newRes;
|
|
}
|
|
}
|
|
|
|
function refreshMetrics() {
|
|
metricsQuery = queryStore({
|
|
client: client,
|
|
query: query,
|
|
variables: { id: jobId, metrics, scopes, selectedResolution },
|
|
// requestPolicy: 'network-only' // use default cache-first for refresh
|
|
});
|
|
}
|
|
|
|
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 {
|
|
name: jobMetric.data.name,
|
|
disabled: checkMetricDisabled(
|
|
jobMetric.data.name,
|
|
job.cluster,
|
|
job.subCluster,
|
|
),
|
|
data: jobMetric.data,
|
|
};
|
|
} else {
|
|
return jobMetric;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<tr>
|
|
<td>
|
|
<JobInfo {job} bind:isSelected showSelect/>
|
|
</td>
|
|
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
|
|
<td colspan={metrics.length}>
|
|
<Card body color="warning">Not monitored or archiving failed</Card>
|
|
</td>
|
|
{:else if $metricsQuery.fetching}
|
|
<td colspan={metrics.length} style="text-align: center;">
|
|
<Spinner secondary />
|
|
</td>
|
|
{:else if $metricsQuery.error}
|
|
<td colspan={metrics.length}>
|
|
<Card body color="danger" class="mb-3">
|
|
{$metricsQuery.error.message.length > 500
|
|
? $metricsQuery.error.message.substring(0, 499) + "..."
|
|
: $metricsQuery.error.message}
|
|
</Card>
|
|
</td>
|
|
{:else}
|
|
{#if showFootprint}
|
|
<td>
|
|
<JobFootprint
|
|
{job}
|
|
width={plotWidth}
|
|
height="{plotHeight}px"
|
|
displayTitle={false}
|
|
/>
|
|
</td>
|
|
{/if}
|
|
{#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric?.name || 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={cluster.find((c) => c.name == job.cluster)}
|
|
subCluster={job.subCluster}
|
|
isShared={job.exclusive != 1}
|
|
numhwthreads={job.numHWThreads}
|
|
numaccs={job.numAcc}
|
|
zoomState={zoomStates[metric.data.name] || null}
|
|
thresholdState={thresholdStates[metric.data.name] || null}
|
|
/>
|
|
{:else if metric.disabled == true && metric.data}
|
|
<Card body color="info"
|
|
>Metric disabled for subcluster <code
|
|
>{metric.data.name}:{job.subCluster}</code
|
|
></Card
|
|
>
|
|
{:else}
|
|
<Card body color="warning">No dataset returned</Card>
|
|
{/if}
|
|
</td>
|
|
{/each}
|
|
{/if}
|
|
</tr>
|