Merge branch 'dev' of github.com:ClusterCockpit/cc-backend into dev

This commit is contained in:
2025-10-13 16:13:53 +02:00
39 changed files with 263 additions and 154 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/cc-backend
/.env
/config.json
/uiConfig.json
/var/job-archive
/var/machine-state

View File

@@ -9,7 +9,7 @@ import "flag"
var (
flagReinitDB, flagInit, flagServer, flagSyncLDAP, flagGops, flagMigrateDB, flagRevertDB,
flagForceDB, flagDev, flagVersion, flagLogDateTime, flagApplyTags bool
flagNewUser, flagDelUser, flagGenJWT, flagConfigFile, flagImportJob, flagLogLevel string
flagNewUser, flagDelUser, flagGenJWT, flagConfigFile, flagUiConfigFile, flagImportJob, flagLogLevel string
)
func cliInit() {
@@ -26,6 +26,7 @@ func cliInit() {
flag.BoolVar(&flagForceDB, "force-db", false, "Force database version, clear dirty flag and exit")
flag.BoolVar(&flagLogDateTime, "logdate", false, "Set this flag to add date and time to log messages")
flag.StringVar(&flagConfigFile, "config", "./config.json", "Specify alternative path to `config.json`")
flag.StringVar(&flagUiConfigFile, "ui-config", "./uiConfig.json", "Specify alternative path to `uiConfig.json`")
flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: <username>:[admin,support,manager,api,user]:<password>")
flag.StringVar(&flagDelUser, "del-user", "", "Remove a existing user. Argument format: <username>")
flag.StringVar(&flagGenJWT, "jwt", "", "Generate and print a JWT for the user specified by its `username`")

View File

@@ -52,6 +52,8 @@ func onFailureResponse(rw http.ResponseWriter, r *http.Request, err error) {
}
func serverInit() {
// Init Web Package (Primarily: uiDefaults)
web.Init(flagUiConfigFile)
// Setup the http.Handler/Router used by the server
graph.Init()
resolver := graph.GetResolverInstance()

View File

@@ -1,12 +0,0 @@
{
"clusters": [
{
"name": "fritz",
"default_metrics": "cpu_load, flops_any, core_power, lustre_open, mem_used, mem_bw, net_bytes_in"
},
{
"name": "alex",
"default_metrics": "flops_any, mem_bw, mem_used, vectorization_ratio"
}
]
}

45
configs/uiConfig.json Normal file
View File

@@ -0,0 +1,45 @@
{
"jobList": {
"usePaging": false,
"showFootprint":false
},
"jobView": {
"showPolarPlot": true,
"showFootprint": true,
"showRoofline": true,
"showStatTable": true
},
"metricConfig": {
"jobListMetrics": ["mem_bw", "flops_dp"],
"jobViewPlotMetrics": ["mem_bw", "flops_dp"],
"jobViewTableMetrics": ["mem_bw", "flops_dp"],
"clusters": [
{
"name": "test",
"subClusters": [
{
"name": "one",
"jobListMetrics": ["mem_used", "flops_sp"]
}
]
}
]
},
"nodeList": {
"usePaging": true
},
"plotConfiguration": {
"plotsPerRow": 3,
"colorBackground": true,
"lineWidth": 3,
"colorScheme": [
"#00bfff",
"#0000ff",
"#ff00ff",
"#ff0000",
"#ff8000",
"#ffff00",
"#80ff00"
]
}
}

View File

@@ -11,6 +11,8 @@ import (
"strings"
)
// DEPRECATED: SUPERSEDED BY NEW USER CONFIG - userConfig.go / web.go
type DefaultMetricsCluster struct {
Name string `json:"name"`
DefaultMetrics string `json:"default_metrics"`

View File

@@ -129,6 +129,7 @@ func (r *UserRepository) AddUser(user *schema.User) error {
cclog.Infof("new user %#v created (roles: %s, auth-source: %d, projects: %s)", user.Username, rolesJson, user.AuthSource, projectsJson)
// DEPRECATED: SUPERSEDED BY NEW USER CONFIG - userConfig.go / web.go
defaultMetricsCfg, err := config.LoadDefaultMetricsConfig()
if err != nil {
cclog.Errorf("Error loading default metrics config: %v", err)
@@ -140,7 +141,8 @@ func (r *UserRepository) AddUser(user *schema.User) error {
cclog.Errorf("Error marshaling default metrics for cluster %s: %v", cluster.Name, err)
continue
}
confKey := "job_view_selectedMetrics:" + cluster.Name
// Note: StatisticsTable now has different key (metricConfig_jobViewTableMetrics): Not updated here.
confKey := "metricConfig_jobViewPlotMetrics:" + cluster.Name
if _, err := sq.Insert("configuration").
Columns("username", "confkey", "value").
Values(user.Username, confKey, string(metricsJSON)).
@@ -151,6 +153,7 @@ func (r *UserRepository) AddUser(user *schema.User) error {
}
}
}
// END DEPRECATION
return nil
}

View File

@@ -91,8 +91,8 @@ func (uCfg *UserCfgRepo) GetUIConfig(user *schema.User) (map[string]any, error)
uiconfig[key] = val
}
// Add global ShortRunningJobsDuration setting as plot_list_hideShortRunningJobs
uiconfig["plot_list_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration
// Add global ShortRunningJobsDuration setting as jobList_hideShortRunningJobs
uiconfig["jobList_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration
return uiconfig, 24 * time.Hour, size
})

View File

@@ -82,27 +82,21 @@
let colWidth1 = $state(0);
let colWidth2 = $state(0);
let jobFilters = $state([]);
let metricsInHistograms = $state(ccconfig.analysis_view_histogramMetrics)
let metricsInScatterplots = $state(ccconfig.analysis_view_scatterPlotMetrics)
let metricsInHistograms = $state(ccconfig?.analysisView_histogramMetrics || [])
let metricsInScatterplots = $state(ccconfig?.analysisView_scatterPlotMetrics || [])
let sortSelection = $state(
sortOptions.find(
(option) =>
option.key ==
ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`],
) ||
sortOptions.find(
(option) => option.key == ccconfig.analysis_view_selectedTopCategory,
)
ccconfig[`analysisView_selectedTopCategory:${filterPresets.cluster}`],
) || sortOptions[0]
);
let groupSelection = $state(
groupOptions.find(
(option) =>
option.key ==
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`],
) ||
groupOptions.find(
(option) => option.key == ccconfig.analysis_view_selectedTopEntity,
)
ccconfig[`analysisView_selectedTopEntity:${filterPresets.cluster}`],
) || groupOptions[0]
);
/* Init Function */
@@ -275,15 +269,15 @@
function updateEntityConfiguration(select) {
if (
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`] !=
ccconfig[`analysisView_selectedTopEntity:${filterPresets.cluster}`] !=
select
) {
updateConfigurationMutation({
name: `analysis_view_selectedTopEntity:${filterPresets.cluster}`,
name: `analysisView_selectedTopEntity:${filterPresets.cluster}`,
value: JSON.stringify(select),
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
// console.log(`analysis_view_selectedTopEntity:${filterPresets.cluster}` + ' -> Updated!')
// console.log(`analysisView_selectedTopEntity:${filterPresets.cluster}` + ' -> Updated!')
} else if (res.fetching === false && res.error) {
throw res.error;
}
@@ -295,15 +289,15 @@
function updateCategoryConfiguration(select) {
if (
ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`] !=
ccconfig[`analysisView_selectedTopCategory:${filterPresets.cluster}`] !=
select
) {
updateConfigurationMutation({
name: `analysis_view_selectedTopCategory:${filterPresets.cluster}`,
name: `analysisView_selectedTopCategory:${filterPresets.cluster}`,
value: JSON.stringify(select),
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
// console.log(`analysis_view_selectedTopCategory:${filterPresets.cluster}` + ' -> Updated!')
// console.log(`analysisView_selectedTopCategory:${filterPresets.cluster}` + ' -> Updated!')
} else if (res.fetching === false && res.error) {
throw res.error;
}
@@ -591,7 +585,7 @@
numBins,
),
}))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
itemsPerRow={ccconfig.plotConfiguration_plotsPerRow}
gridContent={histoGridContent}
/>
</Col>
@@ -634,7 +628,7 @@
(f) => f.metric == m2,
).data,
}))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
itemsPerRow={ccconfig.plotConfiguration_plotsPerRow}
gridContent={metricsGridContent}
/>
</Col>

View File

@@ -69,6 +69,9 @@
`);
const client = getContextClient();
const ccconfig = getContext("cc-config");
const showRoofline = !!ccconfig[`jobView_showRoofline`];
const showStatsTable = !!ccconfig[`jobView_showStatTable`];
/* Note: Actual metric data queried in <Metric> Component, only require base infos here -> reduce backend load by requesting just stats */
const query = gql`
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
@@ -168,8 +171,8 @@
let job = $initq.data.job;
if (!job) return;
const pendingMetrics = (
ccconfig[`job_view_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
ccconfig[`job_view_selectedMetrics:${job.cluster}`]
ccconfig[`metricConfig_jobViewPlotMetrics:${job.cluster}:${job.subCluster}`] ||
ccconfig[`metricConfig_jobViewPlotMetrics:${job.cluster}`]
) ||
$initq.data.globalMetrics.reduce((names, gm) => {
if (gm.availability.find((av) => av.cluster === job.cluster && av.subClusters.includes(job.subCluster))) {
@@ -232,7 +235,7 @@
{/if}
<TabPane tabId="meta-info" tab="Job Info" active={$initq.data?.job?.metaData?.message?false:true}>
<CardBody class="pb-2">
<JobInfo job={$initq.data.job} {username} {authlevel} {roles} showTagedit/>
<JobInfo job={$initq.data.job} {username} {authlevel} {roles} showTagEdit/>
</CardBody>
</TabPane>
{#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
@@ -268,7 +271,9 @@
{#if $initq.error}
<Card body color="danger">{$initq.error.message}</Card>
{:else if $initq?.data}
<JobRoofline job={$initq.data.job} clusters={$initq.data.clusters}/>
{#if showRoofline}
<JobRoofline job={$initq.data.job} clusters={$initq.data.clusters}/>
{/if}
{:else}
<Spinner secondary />
{/if}
@@ -354,14 +359,14 @@
groupByScope($jobMetrics.data.scopedJobStats),
selectedMetrics,
)}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
itemsPerRow={ccconfig.plotConfiguration_plotsPerRow}
{gridContent}
/>
{/if}
</CardBody>
</Card>
<!-- Statistcics Table -->
<!-- Metadata && Statistcics Table -->
<Row class="mb-3">
<Col>
{#if $initq?.data}
@@ -397,8 +402,10 @@
</div>
</TabPane>
{/if}
<!-- Includes <TabPane> Statistics Table with Independent GQL Query -->
<StatsTab job={$initq.data.job} clusters={$initq.data.clusters} tabActive={!somethingMissing}/>
{#if showStatsTable}
<!-- Includes <TabPane> Statistics Table with Independent GQL Query -->
<StatsTab job={$initq.data.job} clusters={$initq.data.clusters} tabActive={!somethingMissing}/>
{/if}
<TabPane tabId="job-script" tab="Job Script">
<div class="pre-wrapper">
{#if $initq.data.job.metaData?.jobScript}
@@ -432,7 +439,7 @@
presetMetrics={selectedMetrics}
cluster={$initq.data.job.cluster}
subCluster={$initq.data.job.subCluster}
configName="job_view_selectedMetrics"
configName="metricConfig_jobViewPlotMetrics"
preInitialized
applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics]

View File

@@ -53,13 +53,13 @@
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
let metrics = $state(filterPresets.cluster
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics
? ccconfig[`metricConfig_jobListMetrics:${filterPresets.cluster}`] ||
ccconfig.metricConfig_jobListMetrics
: ccconfig.metricConfig_jobListMetrics
);
let showFootprint = $state(filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
? !!ccconfig[`jobList_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.jobList_showFootprint
);
/* Functions */
@@ -213,7 +213,7 @@
bind:showFootprint
presetMetrics={metrics}
cluster={selectedCluster}
configName="plot_list_selectedMetrics"
configName="metricConfig_jobListMetrics"
footprintSelect
applyMetrics={(newMetrics) =>
metrics = [...newMetrics]

View File

@@ -136,7 +136,7 @@
startTimeQuickSelect
bind:this={filterComponent}
{filterPresets}
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
menuText="Only {type.toLowerCase()}s with matching jobs will be displayed."
applyFilters={(detail) => {
jobFilters = detail.filters;
}}

View File

@@ -247,7 +247,7 @@
),
}))
.sort((a, b) => a.name.localeCompare(b.name))}
itemsPerRow={ccconfig.plot_view_plotsPerRow}
itemsPerRow={ccconfig.plotConfiguration_plotsPerRow}
{gridContent}
/>
{/if}

View File

@@ -28,7 +28,7 @@
} = $props();
/*Const Init */
const useCbColors = getContext("cc-config")?.plot_general_colorblindMode || false
const useCbColors = getContext("cc-config")?.plotConfiguration_colorblindMode || false
</script>

View File

@@ -21,6 +21,11 @@
Icon,
Button,
} from "@sveltestrap/sveltestrap";
import {
gql,
getContextClient,
mutationStore,
} from "@urql/svelte";
import { init } from "./generic/utils.js";
import NodeOverview from "./systems/NodeOverview.svelte";
@@ -45,6 +50,7 @@
/* Const Init */
const { query: initq } = init();
const client = getContextClient();
const displayNodeOverview = (displayType === 'OVERVIEW');
const ccconfig = getContext("cc-config");
const initialized = getContext("initialized");
@@ -65,20 +71,38 @@
let hostnameFilter = $state("");
let pendingHostnameFilter = $state("");
let isMetricsSelectionOpen = $state(false);
let selectedMetric = $state(ccconfig.system_view_selectedMetric || "");
let selectedMetrics = $state((
ccconfig[`node_list_selectedMetrics:${cluster}:${subCluster}`] ||
ccconfig[`node_list_selectedMetrics:${cluster}`]
) || [ccconfig.system_view_selectedMetric]);
/* Derived States */
const systemMetrics = $derived($initialized ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
const presetSystemUnits = $derived(loadUnits(systemMetrics));
let selectedMetric = $derived.by(() => {
let configKey = `nodeOverview_selectedMetric`;
if (cluster) configKey += `:${cluster}`;
if (subCluster) configKey += `:${subCluster}`;
if ($initialized) {
if (ccconfig[configKey]) return ccconfig[configKey]
else if (systemMetrics.length !== 0) return systemMetrics[0].name
}
return ""
});
let selectedMetrics = $derived.by(() => {
let configKey = `nodeList_selectedMetrics`;
if (cluster) configKey += `:${cluster}`;
if (subCluster) configKey += `:${subCluster}`;
if ($initialized) {
if (ccconfig[configKey]) return ccconfig[configKey]
else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
}
return []
});
/* Effects */
$effect(() => {
// OnMount: Ping Var, without this, OVERVIEW metric select is empty (reason tbd)
systemMetrics
if (displayNodeOverview) {
updateOverviewMetric(selectedMetric)
}
});
/* Functions */
@@ -99,6 +123,34 @@
hostnameFilter = pendingHostnameFilter;
}, 500);
};
function updateOverviewMetric(newMetric) {
let configKey = `nodeOverview_selectedMetric`;
if (cluster) configKey += `:${cluster}`;
if (subCluster) configKey += `:${subCluster}`;
updateConfigurationMutation({
name: configKey,
value: JSON.stringify(newMetric),
}).subscribe((res) => {
if (res.fetching === false && res.error) {
throw res.error;
}
});
};
const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({
client: client,
query: gql`
mutation ($name: String!, $value: String!) {
updateConfiguration(name: $name, value: $value)
}
`,
variables: { name, value },
});
};
</script>
<!-- ROW1: Tools-->
@@ -213,7 +265,7 @@
presetMetrics={selectedMetrics}
{cluster}
{subCluster}
configName="node_list_selectedMetrics"
configName="nodeList_selectedMetrics"
applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics]
}

View File

@@ -65,15 +65,15 @@
let isMetricsSelectionOpen = $state(false);
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
let selectedHistogramsBuffer = $state({ all: (ccconfig['user_view_histogramMetrics'] || []) })
let selectedHistogramsBuffer = $state({ all: (ccconfig['userView_histogramMetrics'] || []) })
let metrics = $state(filterPresets.cluster
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics
? ccconfig[`metricConfig_jobListMetrics:${filterPresets.cluster}`] ||
ccconfig.metricConfig_jobListMetrics
: ccconfig.metricConfig_jobListMetrics
);
let showFootprint = $state(filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
? !!ccconfig[`jobList_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.jobList_showFootprint
);
// Histogram Vars
@@ -129,7 +129,7 @@
/* Effect */
$effect(() => {
if (!selectedHistogramsBuffer[selectedCluster]) {
selectedHistogramsBuffer[selectedCluster] = ccconfig[`user_view_histogramMetrics:${selectedCluster}`];
selectedHistogramsBuffer[selectedCluster] = ccconfig[`userView_histogramMetrics:${selectedCluster}`];
};
});
@@ -138,7 +138,7 @@
filterComponent.updateFilters();
// Why? -> `$derived(ccconfig[$cluster])` only loads array from last Backend-Query if $cluster changed reactively (without reload)
if (filterPresets?.cluster) {
selectedHistogramsBuffer[filterPresets.cluster] = ccconfig[`user_view_histogramMetrics:${filterPresets.cluster}`];
selectedHistogramsBuffer[filterPresets.cluster] = ccconfig[`userView_histogramMetrics:${filterPresets.cluster}`];
};
});
</script>
@@ -393,7 +393,7 @@
bind:showFootprint
presetMetrics={metrics}
cluster={selectedCluster}
configName="plot_list_selectedMetrics"
configName="metricConfig_jobListMetrics"
footprintSelect
applyMetrics={(newMetrics) =>
metrics = [...newMetrics]
@@ -404,7 +404,7 @@
cluster={selectedCluster}
bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms}
configName="user_view_histogramMetrics"
configName="userView_histogramMetrics"
applyChange={(newSelection) => {
selectedHistogramsBuffer[selectedCluster || 'all'] = [...newSelection];
}}

View File

@@ -92,7 +92,7 @@
value={metric}
onchange={() => {
updateConfiguration({
name: "analysis_view_histogramMetrics",
name: "analysisView_histogramMetrics",
value: metricsInHistograms,
});
applyHistograms(metricsInHistograms);
@@ -131,7 +131,7 @@
(p) => pair != p,
);
updateConfiguration({
name: "analysis_view_scatterPlotMetrics",
name: "analysisView_scatterPlotMetrics",
value: metricsInScatterplots,
});
applyScatter(metricsInScatterplots);
@@ -169,7 +169,7 @@
selectedMetric1 = null;
selectedMetric2 = null;
updateConfiguration({
name: "analysis_view_scatterPlotMetrics",
name: "analysisView_scatterPlotMetrics",
value: metricsInScatterplots,
});
applyScatter(metricsInScatterplots);

View File

@@ -24,7 +24,7 @@
/* State Init */
let message = $state({ msg: "", target: "", color: "#d63384" });
let displayMessage = $state(false);
let cbmode = $state(ccconfig?.plot_general_colorblindMode || false);
let cbmode = $state(ccconfig?.plotConfiguration_colorblindMode || false);
/* Functions */
async function handleSettingSubmit(event, setting) {
@@ -38,7 +38,7 @@
const res = await fetch(form.action, { method: "POST", body: formData });
if (res.ok) {
let text = await res.text();
if (formData.get("key") === "plot_general_colorblindMode") {
if (formData.get("key") === "plotConfiguration_colorblindMode") {
cbmode = JSON.parse(formData.get("value"));
}
popMessage(text, target, "#048109");

View File

@@ -69,10 +69,10 @@
method="post"
action="/frontend/configuration/"
onsubmit={(e) => handleSettingSubmit(e, "#node-paging-form", "npag")}>
<input type="hidden" name="key" value="node_list_usePaging" />
<input type="hidden" name="key" value="nodeList_usePaging" />
<div class="mb-3">
<div>
{#if config?.node_list_usePaging}
{#if config?.nodeList_usePaging}
<input type="radio" id="nodes-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="nodes-true" name="value" value="true" />
@@ -80,7 +80,7 @@
<label for="nodes-true">Paging with selectable count of nodes.</label>
</div>
<div>
{#if config?.node_list_usePaging}
{#if config?.nodeList_usePaging}
<input type="radio" id="nodes-false" name="value" value="false" />
{:else}
<input type="radio" id="nodes-false-checked" name="value" value="false" checked />

View File

@@ -29,10 +29,10 @@
} = $props();
/* State Init */
let activeRow = $state(JSON.stringify(config?.plot_general_colorscheme));
let activeRow = $state(JSON.stringify(config?.plotConfiguration_colorScheme));
/* Const Init */
const colorschemes = {
const colorSchemes = {
Default: [
"#00bfff",
"#0000ff",
@@ -266,7 +266,7 @@
// https://personal.sron.nl/~pault/
// https://tsitsul.in/blog/coloropt/
const cvdschemes = {
const cvdSchemes = {
HighContrast: [
"rgb(221,170,51)",
"rgb(187,85,102)",
@@ -344,11 +344,11 @@
</div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorscheme" />
<input type="hidden" name="key" value="plotConfiguration_colorScheme" />
<Table hover>
<tbody>
{#key activeRow}
{#each Object.entries(cbmode ? cvdschemes : colorschemes) as [name, rgbrow]}
{#each Object.entries(cbmode ? cvdSchemes : colorSchemes) as [name, rgbrow]}
<tr>
<th scope="col">{name}</th>
<td>

View File

@@ -55,7 +55,7 @@
</div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_lineWidth" />
<input type="hidden" name="key" value="plotConfiguration_lineWidth" />
<div class="mb-3">
<label for="value" class="form-label">Line Width</label>
<input
@@ -64,7 +64,7 @@
id="lwvalue"
name="value"
aria-describedby="lineWidthHelp"
value={config.plot_general_lineWidth}
value={config.plotConfiguration_lineWidth}
min="1"
/>
<div id="lineWidthHelp" class="form-text">
@@ -102,7 +102,7 @@
</div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_view_plotsPerRow" />
<input type="hidden" name="key" value="plotConfiguration_plotsPerRow" />
<div class="mb-3">
<label for="value" class="form-label">Plots per Row</label>
<input
@@ -111,7 +111,7 @@
id="pprvalue"
name="value"
aria-describedby="plotsperrowHelp"
value={config.plot_view_plotsPerRow}
value={config.plotConfiguration_plotsPerRow}
min="1"
/>
<div id="plotsperrowHelp" class="form-text">
@@ -150,10 +150,10 @@
</div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorBackground" />
<input type="hidden" name="key" value="plotConfiguration_colorBackground" />
<div class="mb-3">
<div>
{#if config.plot_general_colorBackground}
{#if config.plotConfiguration_colorBackground}
<input type="radio" id="colb-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="colb-true" name="value" value="true" />
@@ -161,7 +161,7 @@
<label for="true">Yes</label>
</div>
<div>
{#if config.plot_general_colorBackground}
{#if config.plotConfiguration_colorBackground}
<input type="radio" id="colb-false" name="value" value="false" />
{:else}
<input type="radio" id="colb-false-checked" name="value" value="false" checked />
@@ -196,10 +196,10 @@
</div>
{/if}
</CardTitle>
<input type="hidden" name="key" value="plot_general_colorblindMode" />
<input type="hidden" name="key" value="plotConfiguration_colorblindMode" />
<div class="mb-3">
<div>
{#if config?.plot_general_colorblindMode}
{#if config?.plotConfiguration_colorblindMode}
<input type="radio" id="cbm-true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="cbm-true" name="value" value="true" />
@@ -207,7 +207,7 @@
<label for="true">Yes</label>
</div>
<div>
{#if config?.plot_general_colorblindMode}
{#if config?.plotConfiguration_colorblindMode}
<input type="radio" id="cbm-false" name="value" value="false" />
{:else}
<input type="radio" id="cbm-false-checked" name="value" value="false" checked />

View File

@@ -104,10 +104,10 @@
>
</div>{/if}
</CardTitle>
<input type="hidden" name="key" value="job_list_usePaging" />
<input type="hidden" name="key" value="jobList_usePaging" />
<div class="mb-3">
<div>
{#if config?.job_list_usePaging}
{#if config?.jobList_usePaging}
<input type="radio" id="true-checked" name="value" value="true" checked />
{:else}
<input type="radio" id="true" name="value" value="true" />
@@ -115,7 +115,7 @@
<label for="true">Paging with selectable count of jobs.</label>
</div>
<div>
{#if config?.job_list_usePaging}
{#if config?.jobList_usePaging}
<input type="radio" id="false" name="value" value="false" />
{:else}
<input type="radio" id="false-checked" name="value" value="false" checked />

View File

@@ -294,11 +294,12 @@
Filters
</DropdownToggle>
<DropdownMenu>
<DropdownItem header>Manage Filters</DropdownItem>
{#if menuText}
<DropdownItem header>Note</DropdownItem>
<DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider />
{/if}
<DropdownItem header>Manage Filters</DropdownItem>
<DropdownItem onclick={() => (isClusterOpen = true)}>
<Icon name="cpu" /> Cluster/Partition
</DropdownItem>
@@ -325,7 +326,7 @@
</DropdownItem>
{#if startTimeQuickSelect}
<DropdownItem divider />
<DropdownItem disabled>Start Time Quick Selection</DropdownItem>
<DropdownItem header>Start Time Quick Selection</DropdownItem>
{#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
<DropdownItem
onclick={() => {

View File

@@ -26,7 +26,7 @@
/* Svelte 5 Props */
let {
matchedCompareJobs = $bindable(0),
metrics = getContext("cc-config")?.plot_list_selectedMetrics,
metrics = getContext("cc-config")?.metricConfig_jobListMetrics,
filterBuffer = [],
} = $props();
@@ -112,7 +112,7 @@
// (Re-)query and optionally set new filters; Query will be started reactively.
export function queryJobs(filters) {
if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
let minRunningFor = ccconfig.jobList_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor });
}
@@ -229,7 +229,7 @@
// function updateConfiguration(value, page) {
// updateConfigurationMutation({
// name: "plot_list_jobsPerPage",
// name: "jobList_jobsPerPage",
// value: value,
// }).subscribe((res) => {
// if (res.fetching === false && !res.error) {

View File

@@ -32,7 +32,7 @@
let {
matchedListJobs = $bindable(0),
selectedJobs = $bindable([]),
metrics = getContext("cc-config").plot_list_selectedMetrics,
metrics = getContext("cc-config").metricConfig_jobListMetrics,
sorting = { field: "startTime", type: "col", order: "DESC" },
showFootprint = false,
filterBuffer = [],
@@ -42,7 +42,7 @@
const ccconfig = getContext("cc-config");
const initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics");
const usePaging = ccconfig?.job_list_usePaging || false;
const usePaging = ccconfig?.jobList_usePaging || false;
const jobInfoColumnWidth = 250;
const client = getContextClient();
const query = gql`
@@ -101,7 +101,7 @@
let jobs = $state([]);
let filter = $state([...filterBuffer]);
let page = $state(1);
let itemsPerPage = $state(usePaging ? (ccconfig?.plot_list_jobsPerPage || 10) : 10);
let itemsPerPage = $state(usePaging ? (ccconfig?.jobList_jobsPerPage || 10) : 10);
let triggerMetricRefresh = $state(false);
let tableWidth = $state(0);
@@ -177,7 +177,7 @@
// (Re-)query and optionally set new filters; Query will be started reactively.
export function queryJobs(filters) {
if (filters != null) {
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
let minRunningFor = ccconfig.jobList_hideShortRunningJobs;
if (minRunningFor && minRunningFor > 0) {
filters.push({ minRunningFor });
}
@@ -207,7 +207,7 @@
function updateConfiguration(value, newPage) {
updateConfigurationMutation({
name: "plot_list_jobsPerPage",
name: "jobList_jobsPerPage",
value: value.toString(),
}).subscribe((res) => {
if (res.fetching === false && !res.error) {
@@ -236,10 +236,6 @@
});
};
const equalsCheck = (a, b) => {
return JSON.stringify(a) === JSON.stringify(b);
}
/* Init Header */
stickyHeader(
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",

View File

@@ -170,7 +170,7 @@
<tr>
<td>
<JobInfo {job} bind:isSelected showSelect/>
<JobInfo {job} bind:isSelected showJobSelect/>
</td>
{#if job.monitoringStatus == 0 || job.monitoringStatus == 2}
<td colspan={metrics.length}>

View File

@@ -44,8 +44,8 @@
/* Const Init */
const clusterCockpitConfig = getContext("cc-config");
const lineWidth = clusterCockpitConfig?.plot_general_lineWidth / window.devicePixelRatio || 2;
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
const lineWidth = clusterCockpitConfig?.plotConfiguration_lineWidth / window.devicePixelRatio || 2;
const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
// UPLOT SERIES INIT //
const plotSeries = [

View File

@@ -56,9 +56,9 @@
const resampleConfig = getContext("resampling");
const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster);
const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric);
const lineWidth = clusterCockpitConfig?.plot_general_lineWidth / window.devicePixelRatio || 2;
const lineColors = clusterCockpitConfig?.plot_general_colorscheme || ["#00bfff","#0000ff","#ff00ff","#ff0000","#ff8000","#ffff00","#80ff00"];
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
const lineColors = clusterCockpitConfig.plotConfiguration_colorScheme;
const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth / window.devicePixelRatio;
const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
const renderSleepTime = 200;
const normalLineColor = "#000000";
const backgroundColors = {
@@ -416,7 +416,7 @@
// RETURN BG COLOR FROM THRESHOLD
function backgroundColor() {
if (
clusterCockpitConfig.plot_general_colorBackground == false ||
clusterCockpitConfig.plotConfiguration_colorBackground == false ||
!thresholds ||
!(series && series.every((s) => s.statistics != null))
)

View File

@@ -80,7 +80,7 @@
} = $props();
/* Const Init */
const useCbColors = getContext("cc-config")?.plot_general_colorblindMode || false
const useCbColors = getContext("cc-config")?.plotConfiguration_colorblindMode || false
const options = {
maintainAspectRatio: false,
animation: false,

View File

@@ -41,8 +41,8 @@
} = $props();
/* Const Init */
const lineWidth = clusterCockpitConfig?.plot_general_lineWidth || 2;
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth;
const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
/* Var Init */
let timeoutId = null;

View File

@@ -39,8 +39,8 @@
} = $props();
/* Const Init */
const lineWidth = clusterCockpitConfig?.plot_general_lineWidth || 2;
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
const lineWidth = clusterCockpitConfig.plotConfiguration_lineWidth;
const cbmode = clusterCockpitConfig?.plotConfiguration_colorblindMode || false;
/* Var Init */
let timeoutId = null;

View File

@@ -57,12 +57,12 @@
};
/* State Init */
let pendingMetrics = $state(presetMetrics);
let pendingShowFootprint = $state(!!showFootprint);
let listedMetrics = $state([]);
let columnHovering = $state(null);
/* Derives States */
let pendingMetrics = $derived(presetMetrics);
const allMetrics = $derived(loadAvailable(preInitialized || $initialized));
/* Reactive Effects */
@@ -151,8 +151,8 @@
updateConfigurationMutation({
name:
!cluster
? "plot_list_showFootprint"
: `plot_list_showFootprint:${cluster}`,
? "jobList_showFootprint"
: `jobList_showFootprint:${cluster}`,
value: JSON.stringify(showFootprint),
}).subscribe((res) => {
if (res.fetching === false && res.error) {

View File

@@ -51,4 +51,8 @@ export function formatTime(t, forNode = false) {
}
}
// const equalsCheck = (a, b) => {
// return JSON.stringify(a) === JSON.stringify(b);
// }
// export const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);

View File

@@ -25,20 +25,23 @@
} = $props();
/* Const Init */
const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`];
const showFootprintTab = !!getContext("cc-config")[`jobView_showFootprint`];
const showPolarTab = !!getContext("cc-config")[`jobView_showPolarPlot`];
</script>
<Card class="overflow-auto" style="width: {width}; height: {height}">
<TabContent>
{#if showFootprintTab}
<TabPane tabId="foot" tab="Footprint" active>
<TabPane tabId="foot" tab="Footprint" active={showFootprintTab}>
<!-- Bars CardBody Here-->
<JobFootprintBars {job} />
</TabPane>
{/if}
<TabPane tabId="polar" tab="Polar" active={!showFootprintTab}>
<!-- Polar Plot CardBody Here -->
<JobFootprintPolar {job} />
</TabPane>
{#if showPolarTab}
<TabPane tabId="polar" tab="Polar" active={showPolarTab && !showFootprintTab}>
<!-- Polar Plot CardBody Here -->
<JobFootprintPolar {job} />
</TabPane>
{/if}
</TabContent>
</Card>

View File

@@ -81,9 +81,9 @@
if (!job) return;
const pendingMetrics = (
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}:${job.subCluster}`] ||
getContext("cc-config")[`job_view_nodestats_selectedMetrics:${job.cluster}`]
) || getContext("cc-config")["job_view_nodestats_selectedMetrics"];
getContext("cc-config")[`metricConfig_jobViewTableMetrics:${job.cluster}:${job.subCluster}`] ||
getContext("cc-config")[`metricConfig_jobViewTableMetrics:${job.cluster}`]
) || getContext("cc-config")["metricConfig_jobViewTableMetrics"];
// Select default Scopes to load: Check before if any metric has accelerator scope by default
const accScopeDefault = [...pendingMetrics].some(function (m) {
@@ -152,7 +152,7 @@
presetMetrics={selectedMetrics}
cluster={job.cluster}
subCluster={job.subCluster}
configName="job_view_nodestats_selectedMetrics"
configName="metricConfig_jobViewTableMetrics"
preInitialized
applyMetrics={(newMetrics) =>
selectedMetrics = [...newMetrics]

View File

@@ -48,8 +48,8 @@
/* Derived */
let selectedHistograms = $derived(cluster
? ccconfig[`status_view_selectedHistograms:${cluster}`] || ( ccconfig['status_view_selectedHistograms'] || [] )
: ccconfig['status_view_selectedHistograms'] || []);
? ccconfig[`statusView_selectedHistograms:${cluster}`] || ( ccconfig['statusView_selectedHistograms'] || [] )
: ccconfig['statusView_selectedHistograms'] || []);
// Note: nodeMetrics are requested on configured $timestep resolution
const metricStatusQuery = $derived(queryStore({
@@ -152,7 +152,7 @@
{cluster}
bind:isOpen={isHistogramSelectionOpen}
presetSelectedHistograms={selectedHistograms}
configName="status_view_selectedHistograms"
configName="statusView_selectedHistograms"
applyChange={(newSelection) => {
selectedHistograms = [...newSelection];
}}

View File

@@ -35,7 +35,7 @@
/* Const Init */
const client = getContextClient();
const usePaging = ccconfig?.node_list_usePaging || false;
const usePaging = ccconfig?.nodeList_usePaging || false;
const nodeListQuery = gql`
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
nodeMetricsList(
@@ -88,7 +88,7 @@
/* State Init */
let nodes = $state([]);
let page = $state(1);
let itemsPerPage = $state(usePaging ? (ccconfig?.plot_list_nodesPerPage || 10) : 10);
let itemsPerPage = $state(usePaging ? (ccconfig?.nodeList_nodesPerPage || 10) : 10);
let headerPaddingTop = $state(0);
let matchedNodes = $state(0);
@@ -165,7 +165,7 @@
// Decouple from Job List Paging Params?
function updateConfiguration(newItems, newPage) {
updateConfigurationMutation({
name: "plot_list_nodesPerPage",
name: "nodeList_nodesPerPage",
value: newItems.toString(),
}).subscribe((res) => {
if (res.fetching === false && !res.error) {

View File

@@ -121,7 +121,7 @@
</Row>
{:else if filteredData?.length > 0}
<!-- PlotGrid flattened into this component -->
<Row cols={{ xs: 1, sm: 2, md: 3, lg: ccconfig.plot_view_plotsPerRow}}>
<Row cols={{ xs: 1, sm: 2, md: 3, lg: ccconfig.plotConfiguration_plotsPerRow}}>
{#key selectedMetric}
{#each filteredData as item (item.host)}
<Col class="px-1">

View File

@@ -11,7 +11,9 @@ import (
"encoding/json"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"strings"
"github.com/ClusterCockpit/cc-backend/internal/config"
@@ -109,16 +111,24 @@ var UIDefaultsMap map[string]any
// "analysis_view_scatterPlotMetrics": [][]string{{"flops_any", "mem_bw"}, {"flops_any", "cpu_load"}, {"cpu_load", "mem_bw"}},
// "job_view_nodestats_selectedMetrics": []string{"flops_any", "mem_bw", "mem_used"},
// "plot_list_jobsPerPage": 50,
// "system_view_selectedMetric": "cpu_load",
// "analysis_view_selectedTopEntity": "user",
// "analysis_view_selectedTopCategory": "totalWalltime",
// "status_view_selectedTopUserCategory": "totalJobs",
// "status_view_selectedTopProjectCategory": "totalJobs",
// }
func Init(rawConfig json.RawMessage) error {
var err error
func Init(configFilePath string) error {
var rawConfig json.RawMessage = nil
raw, rerr := os.ReadFile(configFilePath)
if rerr != nil {
if !os.IsNotExist(rerr) {
log.Fatalf("UI-CONFIG ERROR: %v", rerr)
}
} else {
rawConfig = json.RawMessage(raw)
}
var err error
if rawConfig != nil {
config.Validate(configSchema, rawConfig)
if err = json.Unmarshal(rawConfig, &UIDefaults); err != nil {
@@ -129,13 +139,13 @@ func Init(rawConfig json.RawMessage) error {
UIDefaultsMap = make(map[string]any)
UIDefaultsMap["joblist_usePaging"] = UIDefaults.JobList.UsePaging
UIDefaultsMap["joblist_showFootprint"] = UIDefaults.JobList.ShowFootprint
UIDefaultsMap["nodelist_usePaging"] = UIDefaults.NodeList.UsePaging
UIDefaultsMap["jobview_showPolarPlot"] = UIDefaults.JobView.ShowPolarPlot
UIDefaultsMap["jobview_showFootprint"] = UIDefaults.JobView.ShowFootprint
UIDefaultsMap["jobview_showRoofline"] = UIDefaults.JobView.ShowRoofline
UIDefaultsMap["jobview_showStatTable"] = UIDefaults.JobView.ShowStatTable
UIDefaultsMap["jobList_usePaging"] = UIDefaults.JobList.UsePaging
UIDefaultsMap["jobList_showFootprint"] = UIDefaults.JobList.ShowFootprint
UIDefaultsMap["nodeList_usePaging"] = UIDefaults.NodeList.UsePaging
UIDefaultsMap["jobView_showPolarPlot"] = UIDefaults.JobView.ShowPolarPlot
UIDefaultsMap["jobView_showFootprint"] = UIDefaults.JobView.ShowFootprint
UIDefaultsMap["jobView_showRoofline"] = UIDefaults.JobView.ShowRoofline
UIDefaultsMap["jobView_showStatTable"] = UIDefaults.JobView.ShowStatTable
UIDefaultsMap["metricConfig_jobListMetrics"] = UIDefaults.MetricConfig.JobListMetrics
UIDefaultsMap["metricConfig_jobViewPlotMetrics"] = UIDefaults.MetricConfig.JobViewPlotMetrics