mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-01-31 08:11:45 +01:00
Merge pull request #480 from ClusterCockpit/dev
Change web ui defaults and fix ui config json schema
This commit is contained in:
@@ -155,7 +155,7 @@ const configSchema = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "sub-clusters"],
|
"required": ["name"],
|
||||||
"minItems": 1
|
"minItems": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { onMount, getContext } from "svelte";
|
import { untrack, onMount, getContext } from "svelte";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
Row,
|
Row,
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
scrambleNames,
|
scrambleNames,
|
||||||
} from "./generic/utils.js";
|
} from "./generic/utils.js";
|
||||||
import JobList from "./generic/JobList.svelte";
|
import JobList from "./generic/JobList.svelte";
|
||||||
|
import JobCompare from "./generic/JobCompare.svelte";
|
||||||
import Filters from "./generic/Filters.svelte";
|
import Filters from "./generic/Filters.svelte";
|
||||||
import PlotGrid from "./generic/PlotGrid.svelte";
|
import PlotGrid from "./generic/PlotGrid.svelte";
|
||||||
import Histogram from "./generic/plots/Histogram.svelte";
|
import Histogram from "./generic/plots/Histogram.svelte";
|
||||||
@@ -54,37 +55,44 @@
|
|||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const durationBinOptions = ["1m","10m","1h","6h","12h"];
|
const durationBinOptions = ["1m","10m","1h","6h","12h"];
|
||||||
const metricBinOptions = [10, 20, 50, 100];
|
const metricBinOptions = [10, 20, 50, 100];
|
||||||
|
const matchedJobCompareLimit = 500;
|
||||||
|
|
||||||
/* State Init */
|
/* State Init */
|
||||||
// List & Control Vars
|
// List & Control Vars
|
||||||
let filterComponent = $state(); // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
let filterComponent = $state(); // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
||||||
let jobFilters = $state([]);
|
let jobFilters = $state([]);
|
||||||
|
let filterBuffer = $state([]);
|
||||||
let jobList = $state(null);
|
let jobList = $state(null);
|
||||||
let matchedListJobs = $state(0);
|
let matchedListJobs = $state(0);
|
||||||
let isSortingOpen = $state(false);
|
let isSortingOpen = $state(false);
|
||||||
let isMetricsSelectionOpen = $state(false);
|
let isMetricsSelectionOpen = $state(false);
|
||||||
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
||||||
let selectedHistogramsBuffer = $state({ all: (ccconfig['userView_histogramMetrics'] || []) })
|
let selectedHistogramsBuffer = $state({ all: (ccconfig['userView_histogramMetrics'] || []) })
|
||||||
|
let jobCompare = $state(null);
|
||||||
|
let matchedCompareJobs = $state(0);
|
||||||
|
let showCompare = $state(false);
|
||||||
|
let selectedJobs = $state([]);
|
||||||
|
|
||||||
// Histogram Vars
|
// Histogram Vars
|
||||||
let isHistogramSelectionOpen = $state(false);
|
let isHistogramSelectionOpen = $state(false);
|
||||||
let numDurationBins = $state("1h");
|
let numDurationBins = $state("1h");
|
||||||
let numMetricBins = $state(10);
|
let numMetricBins = $state(10);
|
||||||
|
|
||||||
// Compare Vars (TODO)
|
|
||||||
// let jobCompare = $state(null);
|
|
||||||
// let showCompare = $state(false);
|
|
||||||
// let selectedJobs = $state([]);
|
|
||||||
// let filterBuffer = $state([]);
|
|
||||||
// let matchedCompareJobs = $state(0);
|
|
||||||
|
|
||||||
/* Derived */
|
/* Derived */
|
||||||
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
|
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
|
||||||
let metrics = $derived(filterPresets.cluster
|
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
|
||||||
? ccconfig[`metricConfig_jobListMetrics:${filterPresets.cluster}`] ||
|
let metrics = $derived.by(() => {
|
||||||
ccconfig.metricConfig_jobListMetrics
|
if (selectedCluster) {
|
||||||
: ccconfig.metricConfig_jobListMetrics
|
if (selectedSubCluster) {
|
||||||
);
|
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
|
||||||
|
ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
||||||
|
ccconfig.metricConfig_jobListMetrics
|
||||||
|
}
|
||||||
|
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
||||||
|
ccconfig.metricConfig_jobListMetrics
|
||||||
|
}
|
||||||
|
return ccconfig.metricConfig_jobListMetrics
|
||||||
|
});
|
||||||
let showFootprint = $derived(filterPresets.cluster
|
let showFootprint = $derived(filterPresets.cluster
|
||||||
? !!ccconfig[`jobList_showFootprint:${filterPresets.cluster}`]
|
? !!ccconfig[`jobList_showFootprint:${filterPresets.cluster}`]
|
||||||
: !!ccconfig.jobList_showFootprint
|
: !!ccconfig.jobList_showFootprint
|
||||||
@@ -126,18 +134,29 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Effect */
|
/* Functions */
|
||||||
|
function resetJobSelection() {
|
||||||
|
if (filterComponent && selectedJobs.length === 0) {
|
||||||
|
filterComponent.updateFilters({ dbId: [] });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Reactive Effects */
|
||||||
|
$effect(() => {
|
||||||
|
// Reactive : Trigger Effect
|
||||||
|
selectedJobs.length
|
||||||
|
untrack(() => {
|
||||||
|
// Unreactive : Apply Reset w/o starting infinite loop
|
||||||
|
resetJobSelection()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!selectedHistogramsBuffer[selectedCluster]) {
|
if (!selectedHistogramsBuffer[selectedCluster]) {
|
||||||
selectedHistogramsBuffer[selectedCluster] = ccconfig[`userView_histogramMetrics:${selectedCluster}`];
|
selectedHistogramsBuffer[selectedCluster] = ccconfig[`userView_histogramMetrics:${selectedCluster}`];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
// Load Metric-Selection for last selected cluster
|
|
||||||
metrics = selectedCluster ? ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] : ccconfig.metricConfig_jobListMetrics
|
|
||||||
});
|
|
||||||
|
|
||||||
/* On Mount */
|
/* On Mount */
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
filterComponent.updateFilters();
|
filterComponent.updateFilters();
|
||||||
@@ -167,7 +186,7 @@
|
|||||||
<Row cols={{ xs: 1, md: 2, lg: 6}} class="mb-3">
|
<Row cols={{ xs: 1, md: 2, lg: 6}} class="mb-3">
|
||||||
<Col class="mb-2 mb-lg-0">
|
<Col class="mb-2 mb-lg-0">
|
||||||
<ButtonGroup class="w-100">
|
<ButtonGroup class="w-100">
|
||||||
<Button outline color="primary" onclick={() => (isSortingOpen = true)}>
|
<Button outline color="primary" onclick={() => (isSortingOpen = true)} disabled={showCompare}>
|
||||||
<Icon name="sort-up" /> Sorting
|
<Icon name="sort-up" /> Sorting
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -181,208 +200,264 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col lg="4" class="mb-1 mb-lg-0">
|
<Col lg="4" class="mb-1 mb-lg-0">
|
||||||
<Filters
|
<Filters
|
||||||
startTimeQuickSelect
|
|
||||||
bind:this={filterComponent}
|
bind:this={filterComponent}
|
||||||
{filterPresets}
|
{filterPresets}
|
||||||
matchedJobs={matchedListJobs}
|
showFilter={!showCompare}
|
||||||
|
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
|
||||||
|
startTimeQuickSelect
|
||||||
applyFilters={(detail) => {
|
applyFilters={(detail) => {
|
||||||
jobFilters = [...detail.filters, { user: { eq: user.username } }];
|
jobFilters = [...detail.filters, { user: { eq: user.username } }];
|
||||||
selectedCluster = jobFilters[0]?.cluster
|
selectedCluster = jobFilters[0]?.cluster
|
||||||
? jobFilters[0].cluster.eq
|
? jobFilters[0].cluster.eq
|
||||||
: null;
|
: null;
|
||||||
jobList.queryJobs(jobFilters);
|
selectedSubCluster = jobFilters[1]?.partition
|
||||||
|
? jobFilters[1].partition.eq
|
||||||
|
: null;
|
||||||
|
filterBuffer = [...jobFilters]
|
||||||
|
if (showCompare) {
|
||||||
|
jobCompare.queryJobs(jobFilters);
|
||||||
|
} else {
|
||||||
|
jobList.queryJobs(jobFilters);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="mb-2 mb-lg-0">
|
<Col class="mb-2 mb-lg-0">
|
||||||
<InputGroup>
|
{#if !showCompare}
|
||||||
<InputGroupText>
|
<InputGroup>
|
||||||
<Icon name="bar-chart-line-fill" />
|
<InputGroupText>
|
||||||
</InputGroupText>
|
<Icon name="bar-chart-line-fill" />
|
||||||
<InputGroupText>
|
</InputGroupText>
|
||||||
Duration Bin Size
|
<InputGroupText>
|
||||||
</InputGroupText>
|
Duration Bin Size
|
||||||
<Input type="select" bind:value={numDurationBins} style="max-width: 120px;">
|
</InputGroupText>
|
||||||
{#each durationBinOptions as dbin}
|
<Input type="select" bind:value={numDurationBins} style="max-width: 120px;">
|
||||||
<option value={dbin}>{dbin}</option>
|
{#each durationBinOptions as dbin}
|
||||||
{/each}
|
<option value={dbin}>{dbin}</option>
|
||||||
</Input>
|
{/each}
|
||||||
</InputGroup>
|
</Input>
|
||||||
|
</InputGroup>
|
||||||
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="mb-2 mb-lg-0">
|
<Col class="mb-2 mb-lg-0">
|
||||||
<TextFilter
|
{#if !showCompare}
|
||||||
setFilter={(filter) => filterComponent.updateFilters(filter)}
|
<TextFilter
|
||||||
/>
|
{filterBuffer}
|
||||||
|
setFilter={(filter) => filterComponent.updateFilters(filter)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="mb-1 mb-lg-0">
|
<Col class="mb-1 mb-lg-0">
|
||||||
<Refresher onRefresh={() => {
|
{#if !showCompare}
|
||||||
jobList.refreshJobs()
|
<Refresher onRefresh={() => {
|
||||||
jobList.refreshAllMetrics()
|
jobList.refreshJobs()
|
||||||
}} />
|
jobList.refreshAllMetrics()
|
||||||
|
}} />
|
||||||
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- ROW3: Base Information-->
|
<!-- ROW3: Base Information-->
|
||||||
<Row cols={{ xs: 1, md: 3}} class="mb-2">
|
{#if !showCompare}
|
||||||
{#if $stats.error}
|
<Row cols={{ xs: 1, md: 3}} class="mb-2">
|
||||||
<Col>
|
{#if $stats.error}
|
||||||
<Card body color="danger">{$stats.error.message}</Card>
|
|
||||||
</Col>
|
|
||||||
{:else if !$stats.data}
|
|
||||||
<Col>
|
|
||||||
<Spinner secondary />
|
|
||||||
</Col>
|
|
||||||
{:else}
|
|
||||||
<Col>
|
|
||||||
<Table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Username</th>
|
|
||||||
<td>{scrambleNames ? scramble(user.username) : user.username}</td>
|
|
||||||
</tr>
|
|
||||||
{#if user.name}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Name</th>
|
|
||||||
<td>{scrambleNames ? scramble(user.name) : user.name}</td>
|
|
||||||
</tr>
|
|
||||||
{/if}
|
|
||||||
{#if user.email}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Email</th>
|
|
||||||
<td>{user.email}</td>
|
|
||||||
</tr>
|
|
||||||
{/if}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Total Jobs</th>
|
|
||||||
<td>{$stats.data.jobsStatistics[0].totalJobs}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Short Jobs</th>
|
|
||||||
<td>{$stats.data.jobsStatistics[0].shortJobs}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Total Walltime</th>
|
|
||||||
<td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Total Core Hours</th>
|
|
||||||
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</Col>
|
|
||||||
<Col class="px-1">
|
|
||||||
{#key $stats.data.jobsStatistics[0].histDuration}
|
|
||||||
<Histogram
|
|
||||||
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
|
||||||
title="Duration Distribution"
|
|
||||||
xlabel="Job Runtimes"
|
|
||||||
xunit="Runtime"
|
|
||||||
ylabel="Number of Jobs"
|
|
||||||
yunit="Jobs"
|
|
||||||
usesBins
|
|
||||||
xtime
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
</Col>
|
|
||||||
<Col class="px-1">
|
|
||||||
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
|
||||||
<Histogram
|
|
||||||
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
|
||||||
title="Number of Nodes Distribution"
|
|
||||||
xlabel="Allocated Nodes"
|
|
||||||
xunit="Nodes"
|
|
||||||
ylabel="Number of Jobs"
|
|
||||||
yunit="Jobs"
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
</Col>
|
|
||||||
{/if}
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<!-- ROW4+5: Selectable Histograms -->
|
|
||||||
<Row>
|
|
||||||
<Col xs="12" md="3" lg="2" class="mb-2 mb-md-0">
|
|
||||||
<Button
|
|
||||||
outline
|
|
||||||
color="secondary"
|
|
||||||
class="w-100"
|
|
||||||
onclick={() => (isHistogramSelectionOpen = true)}
|
|
||||||
>
|
|
||||||
<Icon name="bar-chart-line" /> Select Histograms
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
<Col xs="12" md="9" lg="10" class="mb-2 mb-md-0">
|
|
||||||
<InputGroup>
|
|
||||||
<InputGroupText>
|
|
||||||
<Icon name="bar-chart-line-fill" />
|
|
||||||
</InputGroupText>
|
|
||||||
<InputGroupText>
|
|
||||||
Metric Bins
|
|
||||||
</InputGroupText>
|
|
||||||
<Input type="select" bind:value={numMetricBins} style="max-width: 120px;">
|
|
||||||
{#each metricBinOptions as mbin}
|
|
||||||
<option value={mbin}>{mbin}</option>
|
|
||||||
{/each}
|
|
||||||
</Input>
|
|
||||||
</InputGroup>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
{#if selectedHistograms?.length > 0}
|
|
||||||
{#if $stats.error}
|
|
||||||
<Row>
|
|
||||||
<Col>
|
<Col>
|
||||||
<Card body color="danger">{$stats.error.message}</Card>
|
<Card body color="danger">{$stats.error.message}</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
{:else if !$stats.data}
|
||||||
{:else if !$stats.data}
|
|
||||||
<Row>
|
|
||||||
<Col>
|
<Col>
|
||||||
<Spinner secondary />
|
<Spinner secondary />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
{:else}
|
||||||
{:else}
|
<Col>
|
||||||
<hr class="my-2"/>
|
<Table>
|
||||||
<!-- Note: Ignore '#snippet' Error in IDE -->
|
<tbody>
|
||||||
{#snippet gridContent(item)}
|
<tr>
|
||||||
<Histogram
|
<th scope="row">Username</th>
|
||||||
data={convert2uplot(item.data)}
|
<td>{scrambleNames ? scramble(user.username) : user.username}</td>
|
||||||
title="Distribution of '{item.metric} ({item.stat})' footprints"
|
</tr>
|
||||||
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
{#if user.name}
|
||||||
xunit={item.unit}
|
<tr>
|
||||||
ylabel="Number of Jobs"
|
<th scope="row">Name</th>
|
||||||
yunit="Jobs"
|
<td>{scrambleNames ? scramble(user.name) : user.name}</td>
|
||||||
usesBins
|
</tr>
|
||||||
enableFlip
|
{/if}
|
||||||
/>
|
{#if user.email}
|
||||||
{/snippet}
|
<tr>
|
||||||
|
<th scope="row">Email</th>
|
||||||
{#key $stats.data.jobsStatistics[0].histMetrics}
|
<td>{user.email}</td>
|
||||||
<PlotGrid
|
</tr>
|
||||||
items={$stats.data.jobsStatistics[0].histMetrics}
|
{/if}
|
||||||
itemsPerRow={3}
|
<tr>
|
||||||
{gridContent}
|
<th scope="row">Total Jobs</th>
|
||||||
/>
|
<td>{$stats.data.jobsStatistics[0].totalJobs}</td>
|
||||||
{/key}
|
</tr>
|
||||||
{/if}
|
<tr>
|
||||||
{:else}
|
<th scope="row">Short Jobs</th>
|
||||||
<Row class="mt-2">
|
<td>{$stats.data.jobsStatistics[0].shortJobs}</td>
|
||||||
<Col>
|
</tr>
|
||||||
<Card body>No footprint histograms selected.</Card>
|
<tr>
|
||||||
</Col>
|
<th scope="row">Total Walltime</th>
|
||||||
|
<td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Total Core Hours</th>
|
||||||
|
<td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</Col>
|
||||||
|
<Col class="px-1">
|
||||||
|
{#key $stats.data.jobsStatistics[0].histDuration}
|
||||||
|
<Histogram
|
||||||
|
data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
|
||||||
|
title="Duration Distribution"
|
||||||
|
xlabel="Job Runtimes"
|
||||||
|
xunit="Runtime"
|
||||||
|
ylabel="Number of Jobs"
|
||||||
|
yunit="Jobs"
|
||||||
|
usesBins
|
||||||
|
xtime
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</Col>
|
||||||
|
<Col class="px-1">
|
||||||
|
{#key $stats.data.jobsStatistics[0].histNumNodes}
|
||||||
|
<Histogram
|
||||||
|
data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
|
||||||
|
title="Number of Nodes Distribution"
|
||||||
|
xlabel="Allocated Nodes"
|
||||||
|
xunit="Nodes"
|
||||||
|
ylabel="Number of Jobs"
|
||||||
|
yunit="Jobs"
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</Col>
|
||||||
|
{/if}
|
||||||
</Row>
|
</Row>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- ROW6: JOB LIST-->
|
<!-- ROW4+5: Selectable Histograms -->
|
||||||
|
{#if !showCompare}
|
||||||
|
<Row>
|
||||||
|
<Col xs="12" md="3" lg="2" class="mb-2 mb-md-0">
|
||||||
|
<Button
|
||||||
|
outline
|
||||||
|
color="secondary"
|
||||||
|
class="w-100"
|
||||||
|
onclick={() => (isHistogramSelectionOpen = true)}
|
||||||
|
>
|
||||||
|
<Icon name="bar-chart-line" /> Select Histograms
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col xs="12" md="9" lg="10" class="mb-2 mb-md-0">
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupText>
|
||||||
|
<Icon name="bar-chart-line-fill" />
|
||||||
|
</InputGroupText>
|
||||||
|
<InputGroupText>
|
||||||
|
Metric Bins
|
||||||
|
</InputGroupText>
|
||||||
|
<Input type="select" bind:value={numMetricBins} style="max-width: 120px;">
|
||||||
|
{#each metricBinOptions as mbin}
|
||||||
|
<option value={mbin}>{mbin}</option>
|
||||||
|
{/each}
|
||||||
|
</Input>
|
||||||
|
</InputGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{#if selectedHistograms?.length > 0}
|
||||||
|
{#if $stats.error}
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Card body color="danger">{$stats.error.message}</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{:else if !$stats.data}
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Spinner secondary />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{:else}
|
||||||
|
<hr class="my-2"/>
|
||||||
|
<!-- Note: Ignore '#snippet' Error in IDE -->
|
||||||
|
{#snippet gridContent(item)}
|
||||||
|
<Histogram
|
||||||
|
data={convert2uplot(item.data)}
|
||||||
|
title="Distribution of '{item.metric} ({item.stat})' footprints"
|
||||||
|
xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
|
||||||
|
xunit={item.unit}
|
||||||
|
ylabel="Number of Jobs"
|
||||||
|
yunit="Jobs"
|
||||||
|
usesBins
|
||||||
|
enableFlip
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#key $stats.data.jobsStatistics[0].histMetrics}
|
||||||
|
<PlotGrid
|
||||||
|
items={$stats.data.jobsStatistics[0].histMetrics}
|
||||||
|
itemsPerRow={3}
|
||||||
|
{gridContent}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<Row class="mt-2">
|
||||||
|
<Col>
|
||||||
|
<Card body>No footprint histograms selected.</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- ROW6: JOB COMPARE TRIGGER-->
|
||||||
<Row class="mt-3">
|
<Row class="mt-3">
|
||||||
|
<Col xs="12" md="3" class="mb-2 mb-md-0">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button color="primary" disabled={(matchedListJobs >= matchedJobCompareLimit && !(selectedJobs.length != 0)) || $initq.fetching} onclick={() => {
|
||||||
|
if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs})
|
||||||
|
showCompare = !showCompare
|
||||||
|
}} >
|
||||||
|
{showCompare ? 'Return to List' :
|
||||||
|
matchedListJobs >= matchedJobCompareLimit && selectedJobs.length == 0
|
||||||
|
? 'Compare Disabled'
|
||||||
|
: 'Compare' + (selectedJobs.length != 0 ? ` ${selectedJobs.length} ` : ' ') + 'Jobs'
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
{#if !showCompare && selectedJobs.length != 0}
|
||||||
|
<Button class="w-auto" color="warning" onclick={() => {
|
||||||
|
selectedJobs = [] // Only empty array, filters handled by reactive reset
|
||||||
|
}}>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</ButtonGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<!-- ROW7: JOB LIST / COMPARE-->
|
||||||
|
<Row class="mt-2">
|
||||||
<Col>
|
<Col>
|
||||||
<JobList
|
{#if !showCompare}
|
||||||
bind:this={jobList}
|
<JobList
|
||||||
bind:matchedListJobs
|
bind:this={jobList}
|
||||||
{metrics}
|
bind:matchedListJobs
|
||||||
{sorting}
|
bind:selectedJobs
|
||||||
{showFootprint}
|
{metrics}
|
||||||
/>
|
{sorting}
|
||||||
|
{showFootprint}
|
||||||
|
{filterBuffer}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<JobCompare
|
||||||
|
bind:this={jobCompare}
|
||||||
|
bind:matchedCompareJobs
|
||||||
|
{metrics}
|
||||||
|
{filterBuffer}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
@@ -399,6 +474,7 @@
|
|||||||
bind:showFootprint
|
bind:showFootprint
|
||||||
presetMetrics={metrics}
|
presetMetrics={metrics}
|
||||||
cluster={selectedCluster}
|
cluster={selectedCluster}
|
||||||
|
subCluster={selectedSubCluster}
|
||||||
configName="metricConfig_jobListMetrics"
|
configName="metricConfig_jobListMetrics"
|
||||||
footprintSelect
|
footprintSelect
|
||||||
applyMetrics={(newMetrics) =>
|
applyMetrics={(newMetrics) =>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ var UIDefaults = WebConfig{
|
|||||||
ShowFootprint: false,
|
ShowFootprint: false,
|
||||||
},
|
},
|
||||||
NodeList: NodeListConfig{
|
NodeList: NodeListConfig{
|
||||||
UsePaging: true,
|
UsePaging: false,
|
||||||
},
|
},
|
||||||
JobView: JobViewConfig{
|
JobView: JobViewConfig{
|
||||||
ShowPolarPlot: true,
|
ShowPolarPlot: true,
|
||||||
@@ -89,8 +89,8 @@ var UIDefaults = WebConfig{
|
|||||||
ShowStatTable: true,
|
ShowStatTable: true,
|
||||||
},
|
},
|
||||||
MetricConfig: MetricConfig{
|
MetricConfig: MetricConfig{
|
||||||
JobListMetrics: []string{"flops_any", "mem_bw", "mem_used"},
|
JobListMetrics: []string{"cpu_load", "flops_any", "mem_bw", "mem_used"},
|
||||||
JobViewPlotMetrics: []string{"flops_any", "mem_bw", "mem_used"},
|
JobViewPlotMetrics: []string{"cpu_load", "flops_any", "mem_bw", "mem_used"},
|
||||||
JobViewTableMetrics: []string{"flops_any", "mem_bw", "mem_used"},
|
JobViewTableMetrics: []string{"flops_any", "mem_bw", "mem_used"},
|
||||||
},
|
},
|
||||||
PlotConfiguration: PlotConfiguration{
|
PlotConfiguration: PlotConfiguration{
|
||||||
|
|||||||
@@ -7,35 +7,48 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
ccconf "github.com/ClusterCockpit/cc-lib/v2/ccConfig"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInit(t *testing.T) {
|
func TestInitDefaults(t *testing.T) {
|
||||||
fp := "../../configs/config.json"
|
// Test Init with nil config uses defaults
|
||||||
ccconf.Init(fp)
|
err := Init(nil)
|
||||||
cfg := ccconf.GetPackageConfig("ui")
|
if err != nil {
|
||||||
|
t.Fatalf("Init failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
Init(cfg)
|
// Check default values are set
|
||||||
|
if UIDefaultsMap["jobList_usePaging"] != false {
|
||||||
if UIDefaultsMap["nodeList_usePaging"] == false {
|
t.Errorf("wrong option\ngot: %v \nwant: false", UIDefaultsMap["jobList_usePaging"])
|
||||||
t.Errorf("wrong option\ngot: %v \nwant: true", UIDefaultsMap["NodeList_UsePaging"])
|
}
|
||||||
|
if UIDefaultsMap["nodeList_usePaging"] != false {
|
||||||
|
t.Errorf("wrong option\ngot: %v \nwant: false", UIDefaultsMap["nodeList_usePaging"])
|
||||||
|
}
|
||||||
|
if UIDefaultsMap["jobView_showPolarPlot"] != true {
|
||||||
|
t.Errorf("wrong option\ngot: %v \nwant: true", UIDefaultsMap["jobView_showPolarPlot"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSimpleDefaults(t *testing.T) {
|
func TestSimpleDefaults(t *testing.T) {
|
||||||
const s = `{
|
const s = `{
|
||||||
"job-list": {
|
"job-list": {
|
||||||
"show-footprint": false
|
"show-footprint": true
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
Init(json.RawMessage(s))
|
err := Init(json.RawMessage(s))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if UIDefaultsMap["jobList_usePaging"] == true {
|
// Verify show-footprint was set
|
||||||
t.Errorf("wrong option\ngot: %v \nwant: false", UIDefaultsMap["NodeList_UsePaging"])
|
if UIDefaultsMap["jobList_showFootprint"] != true {
|
||||||
|
t.Errorf("wrong option\ngot: %v \nwant: true", UIDefaultsMap["jobList_showFootprint"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify other defaults remain unchanged
|
||||||
|
if UIDefaultsMap["jobList_usePaging"] != false {
|
||||||
|
t.Errorf("wrong option\ngot: %v \nwant: false", UIDefaultsMap["jobList_usePaging"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,9 +72,11 @@ func TestOverwrite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
Init(json.RawMessage(s))
|
err := Init(json.RawMessage(s))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("%+v", UIDefaultsMap)
|
|
||||||
v, ok := UIDefaultsMap["metricConfig_jobListMetrics"].([]string)
|
v, ok := UIDefaultsMap["metricConfig_jobListMetrics"].([]string)
|
||||||
if ok {
|
if ok {
|
||||||
if v[0] != "flops_sp" {
|
if v[0] != "flops_sp" {
|
||||||
|
|||||||
Reference in New Issue
Block a user