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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { onMount, getContext } from "svelte";
|
||||
import { untrack, onMount, getContext } from "svelte";
|
||||
import {
|
||||
Table,
|
||||
Row,
|
||||
@@ -33,6 +33,7 @@
|
||||
scrambleNames,
|
||||
} from "./generic/utils.js";
|
||||
import JobList from "./generic/JobList.svelte";
|
||||
import JobCompare from "./generic/JobCompare.svelte";
|
||||
import Filters from "./generic/Filters.svelte";
|
||||
import PlotGrid from "./generic/PlotGrid.svelte";
|
||||
import Histogram from "./generic/plots/Histogram.svelte";
|
||||
@@ -54,37 +55,44 @@
|
||||
const client = getContextClient();
|
||||
const durationBinOptions = ["1m","10m","1h","6h","12h"];
|
||||
const metricBinOptions = [10, 20, 50, 100];
|
||||
const matchedJobCompareLimit = 500;
|
||||
|
||||
/* State Init */
|
||||
// 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 jobFilters = $state([]);
|
||||
let filterBuffer = $state([]);
|
||||
let jobList = $state(null);
|
||||
let matchedListJobs = $state(0);
|
||||
let isSortingOpen = $state(false);
|
||||
let isMetricsSelectionOpen = $state(false);
|
||||
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
||||
let selectedHistogramsBuffer = $state({ all: (ccconfig['userView_histogramMetrics'] || []) })
|
||||
let jobCompare = $state(null);
|
||||
let matchedCompareJobs = $state(0);
|
||||
let showCompare = $state(false);
|
||||
let selectedJobs = $state([]);
|
||||
|
||||
// Histogram Vars
|
||||
let isHistogramSelectionOpen = $state(false);
|
||||
let numDurationBins = $state("1h");
|
||||
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 */
|
||||
let selectedCluster = $derived(filterPresets?.cluster ? filterPresets.cluster : null);
|
||||
let metrics = $derived(filterPresets.cluster
|
||||
? ccconfig[`metricConfig_jobListMetrics:${filterPresets.cluster}`] ||
|
||||
let selectedSubCluster = $derived(filterPresets?.partition ? filterPresets.partition : null);
|
||||
let metrics = $derived.by(() => {
|
||||
if (selectedCluster) {
|
||||
if (selectedSubCluster) {
|
||||
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
|
||||
ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
||||
ccconfig.metricConfig_jobListMetrics
|
||||
: ccconfig.metricConfig_jobListMetrics
|
||||
);
|
||||
}
|
||||
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
|
||||
ccconfig.metricConfig_jobListMetrics
|
||||
}
|
||||
return ccconfig.metricConfig_jobListMetrics
|
||||
});
|
||||
let showFootprint = $derived(filterPresets.cluster
|
||||
? !!ccconfig[`jobList_showFootprint:${filterPresets.cluster}`]
|
||||
: !!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(() => {
|
||||
if (!selectedHistogramsBuffer[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 */
|
||||
onMount(() => {
|
||||
filterComponent.updateFilters();
|
||||
@@ -167,7 +186,7 @@
|
||||
<Row cols={{ xs: 1, md: 2, lg: 6}} class="mb-3">
|
||||
<Col class="mb-2 mb-lg-0">
|
||||
<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
|
||||
</Button>
|
||||
<Button
|
||||
@@ -181,20 +200,30 @@
|
||||
</Col>
|
||||
<Col lg="4" class="mb-1 mb-lg-0">
|
||||
<Filters
|
||||
startTimeQuickSelect
|
||||
bind:this={filterComponent}
|
||||
{filterPresets}
|
||||
matchedJobs={matchedListJobs}
|
||||
showFilter={!showCompare}
|
||||
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
|
||||
startTimeQuickSelect
|
||||
applyFilters={(detail) => {
|
||||
jobFilters = [...detail.filters, { user: { eq: user.username } }];
|
||||
selectedCluster = jobFilters[0]?.cluster
|
||||
? jobFilters[0].cluster.eq
|
||||
: null;
|
||||
selectedSubCluster = jobFilters[1]?.partition
|
||||
? jobFilters[1].partition.eq
|
||||
: null;
|
||||
filterBuffer = [...jobFilters]
|
||||
if (showCompare) {
|
||||
jobCompare.queryJobs(jobFilters);
|
||||
} else {
|
||||
jobList.queryJobs(jobFilters);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col class="mb-2 mb-lg-0">
|
||||
{#if !showCompare}
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
<Icon name="bar-chart-line-fill" />
|
||||
@@ -208,22 +237,29 @@
|
||||
{/each}
|
||||
</Input>
|
||||
</InputGroup>
|
||||
{/if}
|
||||
</Col>
|
||||
<Col class="mb-2 mb-lg-0">
|
||||
{#if !showCompare}
|
||||
<TextFilter
|
||||
{filterBuffer}
|
||||
setFilter={(filter) => filterComponent.updateFilters(filter)}
|
||||
/>
|
||||
{/if}
|
||||
</Col>
|
||||
<Col class="mb-1 mb-lg-0">
|
||||
{#if !showCompare}
|
||||
<Refresher onRefresh={() => {
|
||||
jobList.refreshJobs()
|
||||
jobList.refreshAllMetrics()
|
||||
}} />
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- ROW3: Base Information-->
|
||||
<Row cols={{ xs: 1, md: 3}} class="mb-2">
|
||||
{#if !showCompare}
|
||||
<Row cols={{ xs: 1, md: 3}} class="mb-2">
|
||||
{#if $stats.error}
|
||||
<Col>
|
||||
<Card body color="danger">{$stats.error.message}</Card>
|
||||
@@ -298,10 +334,12 @@
|
||||
{/key}
|
||||
</Col>
|
||||
{/if}
|
||||
</Row>
|
||||
</Row>
|
||||
{/if}
|
||||
|
||||
<!-- ROW4+5: Selectable Histograms -->
|
||||
<Row>
|
||||
{#if !showCompare}
|
||||
<Row>
|
||||
<Col xs="12" md="3" lg="2" class="mb-2 mb-md-0">
|
||||
<Button
|
||||
outline
|
||||
@@ -327,8 +365,8 @@
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
{#if selectedHistograms?.length > 0}
|
||||
</Row>
|
||||
{#if selectedHistograms?.length > 0}
|
||||
{#if $stats.error}
|
||||
<Row>
|
||||
<Col>
|
||||
@@ -365,24 +403,61 @@
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
{:else}
|
||||
{:else}
|
||||
<Row class="mt-2">
|
||||
<Col>
|
||||
<Card body>No footprint histograms selected.</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- ROW6: JOB LIST-->
|
||||
<!-- ROW6: JOB COMPARE TRIGGER-->
|
||||
<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>
|
||||
{#if !showCompare}
|
||||
<JobList
|
||||
bind:this={jobList}
|
||||
bind:matchedListJobs
|
||||
bind:selectedJobs
|
||||
{metrics}
|
||||
{sorting}
|
||||
{showFootprint}
|
||||
{filterBuffer}
|
||||
/>
|
||||
{:else}
|
||||
<JobCompare
|
||||
bind:this={jobCompare}
|
||||
bind:matchedCompareJobs
|
||||
{metrics}
|
||||
{filterBuffer}
|
||||
/>
|
||||
{/if}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -399,6 +474,7 @@
|
||||
bind:showFootprint
|
||||
presetMetrics={metrics}
|
||||
cluster={selectedCluster}
|
||||
subCluster={selectedSubCluster}
|
||||
configName="metricConfig_jobListMetrics"
|
||||
footprintSelect
|
||||
applyMetrics={(newMetrics) =>
|
||||
|
||||
@@ -80,7 +80,7 @@ var UIDefaults = WebConfig{
|
||||
ShowFootprint: false,
|
||||
},
|
||||
NodeList: NodeListConfig{
|
||||
UsePaging: true,
|
||||
UsePaging: false,
|
||||
},
|
||||
JobView: JobViewConfig{
|
||||
ShowPolarPlot: true,
|
||||
@@ -89,8 +89,8 @@ var UIDefaults = WebConfig{
|
||||
ShowStatTable: true,
|
||||
},
|
||||
MetricConfig: MetricConfig{
|
||||
JobListMetrics: []string{"flops_any", "mem_bw", "mem_used"},
|
||||
JobViewPlotMetrics: []string{"flops_any", "mem_bw", "mem_used"},
|
||||
JobListMetrics: []string{"cpu_load", "flops_any", "mem_bw", "mem_used"},
|
||||
JobViewPlotMetrics: []string{"cpu_load", "flops_any", "mem_bw", "mem_used"},
|
||||
JobViewTableMetrics: []string{"flops_any", "mem_bw", "mem_used"},
|
||||
},
|
||||
PlotConfiguration: PlotConfiguration{
|
||||
|
||||
@@ -7,35 +7,48 @@ package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
ccconf "github.com/ClusterCockpit/cc-lib/v2/ccConfig"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
fp := "../../configs/config.json"
|
||||
ccconf.Init(fp)
|
||||
cfg := ccconf.GetPackageConfig("ui")
|
||||
func TestInitDefaults(t *testing.T) {
|
||||
// Test Init with nil config uses defaults
|
||||
err := Init(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
Init(cfg)
|
||||
|
||||
if UIDefaultsMap["nodeList_usePaging"] == false {
|
||||
t.Errorf("wrong option\ngot: %v \nwant: true", UIDefaultsMap["NodeList_UsePaging"])
|
||||
// Check default values are set
|
||||
if UIDefaultsMap["jobList_usePaging"] != false {
|
||||
t.Errorf("wrong option\ngot: %v \nwant: false", UIDefaultsMap["jobList_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) {
|
||||
const s = `{
|
||||
"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 {
|
||||
t.Errorf("wrong option\ngot: %v \nwant: false", UIDefaultsMap["NodeList_UsePaging"])
|
||||
// Verify show-footprint was set
|
||||
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)
|
||||
if ok {
|
||||
if v[0] != "flops_sp" {
|
||||
|
||||
Reference in New Issue
Block a user