Merge pull request #480 from ClusterCockpit/dev

Change web ui defaults and fix ui config json schema
This commit is contained in:
Jan Eitzinger
2026-01-30 18:20:02 +01:00
committed by GitHub
4 changed files with 304 additions and 213 deletions

View File

@@ -155,7 +155,7 @@ const configSchema = `{
} }
} }
}, },
"required": ["name", "sub-clusters"], "required": ["name"],
"minItems": 1 "minItems": 1
} }
} }

View File

@@ -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(() => {
if (selectedCluster) {
if (selectedSubCluster) {
return ccconfig[`metricConfig_jobListMetrics:${selectedCluster}:${selectedSubCluster}`] ||
ccconfig[`metricConfig_jobListMetrics:${selectedCluster}`] ||
ccconfig.metricConfig_jobListMetrics ccconfig.metricConfig_jobListMetrics
: 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,20 +200,30 @@
</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;
selectedSubCluster = jobFilters[1]?.partition
? jobFilters[1].partition.eq
: null;
filterBuffer = [...jobFilters]
if (showCompare) {
jobCompare.queryJobs(jobFilters);
} else {
jobList.queryJobs(jobFilters); jobList.queryJobs(jobFilters);
}
}} }}
/> />
</Col> </Col>
<Col class="mb-2 mb-lg-0"> <Col class="mb-2 mb-lg-0">
{#if !showCompare}
<InputGroup> <InputGroup>
<InputGroupText> <InputGroupText>
<Icon name="bar-chart-line-fill" /> <Icon name="bar-chart-line-fill" />
@@ -208,22 +237,29 @@
{/each} {/each}
</Input> </Input>
</InputGroup> </InputGroup>
{/if}
</Col> </Col>
<Col class="mb-2 mb-lg-0"> <Col class="mb-2 mb-lg-0">
{#if !showCompare}
<TextFilter <TextFilter
{filterBuffer}
setFilter={(filter) => filterComponent.updateFilters(filter)} setFilter={(filter) => filterComponent.updateFilters(filter)}
/> />
{/if}
</Col> </Col>
<Col class="mb-1 mb-lg-0"> <Col class="mb-1 mb-lg-0">
{#if !showCompare}
<Refresher onRefresh={() => { <Refresher onRefresh={() => {
jobList.refreshJobs() jobList.refreshJobs()
jobList.refreshAllMetrics() jobList.refreshAllMetrics()
}} /> }} />
{/if}
</Col> </Col>
</Row> </Row>
<!-- ROW3: Base Information--> <!-- 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} {#if $stats.error}
<Col> <Col>
<Card body color="danger">{$stats.error.message}</Card> <Card body color="danger">{$stats.error.message}</Card>
@@ -298,10 +334,12 @@
{/key} {/key}
</Col> </Col>
{/if} {/if}
</Row> </Row>
{/if}
<!-- ROW4+5: Selectable Histograms --> <!-- ROW4+5: Selectable Histograms -->
<Row> {#if !showCompare}
<Row>
<Col xs="12" md="3" lg="2" class="mb-2 mb-md-0"> <Col xs="12" md="3" lg="2" class="mb-2 mb-md-0">
<Button <Button
outline outline
@@ -327,8 +365,8 @@
</Input> </Input>
</InputGroup> </InputGroup>
</Col> </Col>
</Row> </Row>
{#if selectedHistograms?.length > 0} {#if selectedHistograms?.length > 0}
{#if $stats.error} {#if $stats.error}
<Row> <Row>
<Col> <Col>
@@ -365,24 +403,61 @@
/> />
{/key} {/key}
{/if} {/if}
{:else} {:else}
<Row class="mt-2"> <Row class="mt-2">
<Col> <Col>
<Card body>No footprint histograms selected.</Card> <Card body>No footprint histograms selected.</Card>
</Col> </Col>
</Row> </Row>
{/if}
{/if} {/if}
<!-- ROW6: JOB LIST--> <!-- 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>
{#if !showCompare}
<JobList <JobList
bind:this={jobList} bind:this={jobList}
bind:matchedListJobs bind:matchedListJobs
bind:selectedJobs
{metrics} {metrics}
{sorting} {sorting}
{showFootprint} {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) =>

View File

@@ -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{

View File

@@ -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" {