mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-08-08 14:27:14 +02:00
Merge branch 'migrate_svelte5' into dev
This commit is contained in:
@@ -31,7 +31,8 @@ func (r *clusterResolver) Partitions(ctx context.Context, obj *schema.Cluster) (
|
|||||||
|
|
||||||
// StartTime is the resolver for the startTime field.
|
// StartTime is the resolver for the startTime field.
|
||||||
func (r *jobResolver) StartTime(ctx context.Context, obj *schema.Job) (*time.Time, error) {
|
func (r *jobResolver) StartTime(ctx context.Context, obj *schema.Job) (*time.Time, error) {
|
||||||
panic(fmt.Errorf("not implemented: StartTime - startTime"))
|
timestamp := time.Unix(obj.StartTime, 0)
|
||||||
|
return ×tamp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags is the resolver for the tags field.
|
// Tags is the resolver for the tags field.
|
||||||
|
|||||||
@@ -850,7 +850,7 @@ func (ccms *CCMetricStore) LoadNodeListData(
|
|||||||
if len(nodes) > page.ItemsPerPage {
|
if len(nodes) > page.ItemsPerPage {
|
||||||
start := (page.Page - 1) * page.ItemsPerPage
|
start := (page.Page - 1) * page.ItemsPerPage
|
||||||
end := start + page.ItemsPerPage
|
end := start + page.ItemsPerPage
|
||||||
if end > len(nodes) {
|
if end >= len(nodes) {
|
||||||
end = len(nodes)
|
end = len(nodes)
|
||||||
hasNextPage = false
|
hasNextPage = false
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ func (pdb *PrometheusDataRepository) LoadNodeListData(
|
|||||||
if len(nodes) > page.ItemsPerPage {
|
if len(nodes) > page.ItemsPerPage {
|
||||||
start := (page.Page - 1) * page.ItemsPerPage
|
start := (page.Page - 1) * page.ItemsPerPage
|
||||||
end := start + page.ItemsPerPage
|
end := start + page.ItemsPerPage
|
||||||
if end > len(nodes) {
|
if end >= len(nodes) {
|
||||||
end = len(nodes)
|
end = len(nodes)
|
||||||
hasNextPage = false
|
hasNextPage = false
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func setupNodeRoute(i InfoType, r *http.Request) InfoType {
|
|||||||
i["hostname"] = vars["hostname"]
|
i["hostname"] = vars["hostname"]
|
||||||
i["id"] = fmt.Sprintf("%s (%s)", vars["cluster"], vars["hostname"])
|
i["id"] = fmt.Sprintf("%s (%s)", vars["cluster"], vars["hostname"])
|
||||||
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
|
from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to")
|
||||||
if from != "" || to != "" {
|
if from != "" && to != "" {
|
||||||
i["from"] = from
|
i["from"] = from
|
||||||
i["to"] = to
|
i["to"] = to
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+289
-402
File diff suppressed because it is too large
Load Diff
+13
-14
@@ -7,25 +7,24 @@
|
|||||||
"dev": "rollup -c -w"
|
"dev": "rollup -c -w"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^25.0.8",
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@timohausmann/quadtree-js": "^1.2.6",
|
"@timohausmann/quadtree-js": "^1.2.6",
|
||||||
"rollup": "^4.27.4",
|
"rollup": "^4.41.1",
|
||||||
"rollup-plugin-css-only": "^4.5.2",
|
"rollup-plugin-css-only": "^4.5.2",
|
||||||
"rollup-plugin-svelte": "^7.2.2",
|
"rollup-plugin-svelte": "^7.2.2",
|
||||||
"svelte": "^4.2.19"
|
"svelte": "^5.33.14"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rollup/plugin-replace": "^5.0.7",
|
"@rollup/plugin-replace": "^6.0.2",
|
||||||
"@sveltestrap/sveltestrap": "^6.2.7",
|
"@sveltestrap/sveltestrap": "^7.1.0",
|
||||||
"@urql/svelte": "^4.2.2",
|
"@urql/svelte": "^4.2.3",
|
||||||
"chart.js": "^4.4.6",
|
"chart.js": "^4.4.9",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^4.1.0",
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.11.0",
|
||||||
"mathjs": "^12.4.3",
|
"mathjs": "^14.5.2",
|
||||||
"svelte-chartjs": "^3.1.5",
|
"uplot": "^1.6.32",
|
||||||
"uplot": "^1.6.31",
|
"wonka": "^6.3.5"
|
||||||
"wonka": "^6.3.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,20 @@ import terser from '@rollup/plugin-terser';
|
|||||||
import css from 'rollup-plugin-css-only';
|
import css from 'rollup-plugin-css-only';
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH;
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
// const production = false
|
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
svelte({
|
svelte({
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
// enable run-time checks when not in production
|
// Enable run-time checks when not in production
|
||||||
dev: !production
|
dev: !production,
|
||||||
|
// Enable Svelte 5-specific features
|
||||||
|
hydratable: true, // If using server-side rendering
|
||||||
|
immutable: true, // Optimize updates for immutable data
|
||||||
|
// As of sveltestrap 7.1.0, filtered warnings would appear for imported sveltestrap components
|
||||||
|
warningFilter: (warning) => (
|
||||||
|
warning.code !== 'element_invalid_self_closing_tag' &&
|
||||||
|
warning.code !== 'a11y_interactive_supports_focus'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -23,7 +30,7 @@ const plugins = [
|
|||||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||||
resolve({
|
resolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
dedupe: ['svelte']
|
dedupe: ['svelte', '@sveltejs/kit'] // Ensure deduplication for Svelte 5
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
|
|
||||||
@@ -32,8 +39,10 @@ const plugins = [
|
|||||||
production && terser(),
|
production && terser(),
|
||||||
|
|
||||||
replace({
|
replace({
|
||||||
"process.env.NODE_ENV": JSON.stringify("development"),
|
preventAssignment: true,
|
||||||
preventAssignment: true
|
values: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(production ? "production" : "development"),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -37,14 +37,12 @@
|
|||||||
import ScatterPlot from "./generic/plots/Scatter.svelte";
|
import ScatterPlot from "./generic/plots/Scatter.svelte";
|
||||||
import RooflineHeatmap from "./generic/plots/RooflineHeatmap.svelte";
|
import RooflineHeatmap from "./generic/plots/RooflineHeatmap.svelte";
|
||||||
|
|
||||||
const { query: initq } = init();
|
/* Svelte 5 Props */
|
||||||
|
let { filterPresets } = $props();
|
||||||
export let filterPresets;
|
|
||||||
|
|
||||||
// By default, look at the jobs of the last 6 hours:
|
// By default, look at the jobs of the last 6 hours:
|
||||||
if (filterPresets?.startTime == null) {
|
if (filterPresets?.startTime == null) {
|
||||||
if (filterPresets == null) filterPresets = {};
|
if (filterPresets == null) filterPresets = {};
|
||||||
|
|
||||||
let now = new Date(Date.now());
|
let now = new Date(Date.now());
|
||||||
let hourAgo = new Date(now);
|
let hourAgo = new Date(now);
|
||||||
hourAgo.setHours(hourAgo.getHours() - 6);
|
hourAgo.setHours(hourAgo.getHours() - 6);
|
||||||
@@ -54,27 +52,10 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let cluster;
|
/* Const Init */
|
||||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
const { query: initq } = init();
|
||||||
let jobFilters = [];
|
const client = getContextClient();
|
||||||
let rooflineMaxY;
|
|
||||||
let colWidth1, colWidth2;
|
|
||||||
let numBins = 50;
|
|
||||||
let maxY = -1;
|
|
||||||
|
|
||||||
const initialized = getContext("initialized");
|
|
||||||
const globalMetrics = getContext("globalMetrics");
|
|
||||||
const ccconfig = getContext("cc-config");
|
const ccconfig = getContext("cc-config");
|
||||||
|
|
||||||
let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
|
|
||||||
metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics;
|
|
||||||
|
|
||||||
$: metrics = [
|
|
||||||
...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()]),
|
|
||||||
];
|
|
||||||
|
|
||||||
$: clusterName = cluster?.name ? cluster.name : cluster;
|
|
||||||
|
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
{ key: "totalWalltime", label: "Walltime" },
|
{ key: "totalWalltime", label: "Walltime" },
|
||||||
{ key: "totalNodeHours", label: "Node Hours" },
|
{ key: "totalNodeHours", label: "Node Hours" },
|
||||||
@@ -86,7 +67,22 @@
|
|||||||
{ key: "project", label: "Project ID" },
|
{ key: "project", label: "Project ID" },
|
||||||
];
|
];
|
||||||
|
|
||||||
let sortSelection =
|
/* Var Init */
|
||||||
|
let metricUnits = {};
|
||||||
|
let metricScopes = {};
|
||||||
|
let numBins = 50;
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
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 cluster = $state(filterPresets?.cluster);
|
||||||
|
let rooflineMaxY = $state(0);
|
||||||
|
let maxY = $state(-1);
|
||||||
|
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 sortSelection = $state(
|
||||||
sortOptions.find(
|
sortOptions.find(
|
||||||
(option) =>
|
(option) =>
|
||||||
option.key ==
|
option.key ==
|
||||||
@@ -94,17 +90,20 @@
|
|||||||
) ||
|
) ||
|
||||||
sortOptions.find(
|
sortOptions.find(
|
||||||
(option) => option.key == ccconfig.analysis_view_selectedTopCategory,
|
(option) => option.key == ccconfig.analysis_view_selectedTopCategory,
|
||||||
);
|
)
|
||||||
let groupSelection =
|
);
|
||||||
groupOptions.find(
|
let groupSelection = $state(
|
||||||
(option) =>
|
groupOptions.find(
|
||||||
option.key ==
|
(option) =>
|
||||||
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`],
|
option.key ==
|
||||||
) ||
|
ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`],
|
||||||
groupOptions.find(
|
) ||
|
||||||
(option) => option.key == ccconfig.analysis_view_selectedTopEntity,
|
groupOptions.find(
|
||||||
);
|
(option) => option.key == ccconfig.analysis_view_selectedTopEntity,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Init Function */
|
||||||
getContext("on-init")(({ data }) => {
|
getContext("on-init")(({ data }) => {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
cluster = data.clusters.find((c) => c.name == filterPresets.cluster);
|
cluster = data.clusters.find((c) => c.name == filterPresets.cluster);
|
||||||
@@ -121,120 +120,145 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = getContextClient();
|
/* Derived Vars */
|
||||||
|
const clusterName = $derived(cluster?.name ? cluster.name : cluster);
|
||||||
|
const availableMetrics = $derived(loadAvailable($initq?.data?.globalMetrics, clusterName));
|
||||||
|
const metrics = $derived(
|
||||||
|
[...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()])]
|
||||||
|
);
|
||||||
|
|
||||||
$: statsQuery = queryStore({
|
let statsQuery = $derived(
|
||||||
client: client,
|
queryStore({
|
||||||
query: gql`
|
client: client,
|
||||||
query ($jobFilters: [JobFilter!]!) {
|
query: gql`
|
||||||
stats: jobsStatistics(filter: $jobFilters) {
|
query ($jobFilters: [JobFilter!]!) {
|
||||||
totalJobs
|
stats: jobsStatistics(filter: $jobFilters) {
|
||||||
shortJobs
|
totalJobs
|
||||||
totalWalltime
|
shortJobs
|
||||||
totalNodeHours
|
totalWalltime
|
||||||
totalCoreHours
|
totalNodeHours
|
||||||
totalAccHours
|
totalCoreHours
|
||||||
histDuration {
|
totalAccHours
|
||||||
count
|
histDuration {
|
||||||
value
|
count
|
||||||
}
|
value
|
||||||
histNumCores {
|
}
|
||||||
count
|
histNumCores {
|
||||||
value
|
count
|
||||||
|
value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
`,
|
||||||
`,
|
variables: { jobFilters },
|
||||||
variables: { jobFilters },
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
$: topQuery = queryStore({
|
let topQuery = $derived(
|
||||||
client: client,
|
queryStore({
|
||||||
query: gql`
|
client: client,
|
||||||
query (
|
query: gql`
|
||||||
$jobFilters: [JobFilter!]!
|
query (
|
||||||
$paging: PageRequest!
|
$jobFilters: [JobFilter!]!
|
||||||
$sortBy: SortByAggregate!
|
$paging: PageRequest!
|
||||||
$groupBy: Aggregate!
|
$sortBy: SortByAggregate!
|
||||||
) {
|
$groupBy: Aggregate!
|
||||||
topList: jobsStatistics(
|
|
||||||
filter: $jobFilters
|
|
||||||
page: $paging
|
|
||||||
sortBy: $sortBy
|
|
||||||
groupBy: $groupBy
|
|
||||||
) {
|
) {
|
||||||
id
|
topList: jobsStatistics(
|
||||||
name
|
filter: $jobFilters
|
||||||
totalWalltime
|
page: $paging
|
||||||
totalNodeHours
|
sortBy: $sortBy
|
||||||
totalCoreHours
|
groupBy: $groupBy
|
||||||
totalAccHours
|
) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
totalWalltime
|
||||||
|
totalNodeHours
|
||||||
|
totalCoreHours
|
||||||
|
totalAccHours
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
`,
|
||||||
`,
|
variables: {
|
||||||
variables: {
|
jobFilters,
|
||||||
jobFilters,
|
paging: { itemsPerPage: 10, page: 1 },
|
||||||
paging: { itemsPerPage: 10, page: 1 },
|
sortBy: sortSelection.key.toUpperCase(),
|
||||||
sortBy: sortSelection.key.toUpperCase(),
|
groupBy: groupSelection.key.toUpperCase(),
|
||||||
groupBy: groupSelection.key.toUpperCase(),
|
},
|
||||||
},
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
// Note: Different footprints than those saved in DB per Job -> Caused by Legacy Naming
|
// Note: Different footprints than those saved in DB per Job -> Caused by Legacy Naming
|
||||||
$: footprintsQuery = queryStore({
|
let footprintsQuery = $derived(
|
||||||
client: client,
|
queryStore({
|
||||||
query: gql`
|
client: client,
|
||||||
query ($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
|
query: gql`
|
||||||
footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
|
query ($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
|
||||||
timeWeights {
|
footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
|
||||||
nodeHours
|
timeWeights {
|
||||||
accHours
|
nodeHours
|
||||||
coreHours
|
accHours
|
||||||
}
|
coreHours
|
||||||
metrics {
|
}
|
||||||
metric
|
metrics {
|
||||||
data
|
metric
|
||||||
|
data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
`,
|
||||||
`,
|
variables: { jobFilters, metrics },
|
||||||
variables: { jobFilters, metrics },
|
})
|
||||||
});
|
);
|
||||||
|
|
||||||
|
let rooflineQuery = $derived(
|
||||||
|
queryStore({
|
||||||
|
client: client,
|
||||||
|
query: gql`
|
||||||
|
query (
|
||||||
|
$jobFilters: [JobFilter!]!
|
||||||
|
$rows: Int!
|
||||||
|
$cols: Int!
|
||||||
|
$minX: Float!
|
||||||
|
$minY: Float!
|
||||||
|
$maxX: Float!
|
||||||
|
$maxY: Float!
|
||||||
|
) {
|
||||||
|
rooflineHeatmap(
|
||||||
|
filter: $jobFilters
|
||||||
|
rows: $rows
|
||||||
|
cols: $cols
|
||||||
|
minX: $minX
|
||||||
|
minY: $minY
|
||||||
|
maxX: $maxX
|
||||||
|
maxY: $maxY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
jobFilters,
|
||||||
|
rows: 50,
|
||||||
|
cols: 50,
|
||||||
|
minX: 0.01,
|
||||||
|
minY: 1,
|
||||||
|
maxX: 1000,
|
||||||
|
maxY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
$: rooflineQuery = queryStore({
|
/* Reactive Effects */
|
||||||
client: client,
|
$effect(() => {
|
||||||
query: gql`
|
loadUnitsAndScopes(availableMetrics.length, availableMetrics);
|
||||||
query (
|
});
|
||||||
$jobFilters: [JobFilter!]!
|
$effect(() => {
|
||||||
$rows: Int!
|
updateEntityConfiguration(groupSelection.key);
|
||||||
$cols: Int!
|
});
|
||||||
$minX: Float!
|
$effect(() => {
|
||||||
$minY: Float!
|
updateCategoryConfiguration(sortSelection.key);
|
||||||
$maxX: Float!
|
|
||||||
$maxY: Float!
|
|
||||||
) {
|
|
||||||
rooflineHeatmap(
|
|
||||||
filter: $jobFilters
|
|
||||||
rows: $rows
|
|
||||||
cols: $cols
|
|
||||||
minX: $minX
|
|
||||||
minY: $minY
|
|
||||||
maxX: $maxX
|
|
||||||
maxY: $maxY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: {
|
|
||||||
jobFilters,
|
|
||||||
rows: 50,
|
|
||||||
cols: 50,
|
|
||||||
minX: 0.01,
|
|
||||||
minY: 1,
|
|
||||||
maxX: 1000,
|
|
||||||
maxY,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
@@ -287,22 +311,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let availableMetrics = [];
|
function loadAvailable(globals, name) {
|
||||||
let metricUnits = {};
|
const availableMetrics = new Set();
|
||||||
let metricScopes = {};
|
if (globals && globals.length > 0) {
|
||||||
function loadMetrics(isInitialized) {
|
for (let gm of globals) {
|
||||||
if (!isInitialized) return
|
if (gm.availability.find((av) => av.cluster == name)) {
|
||||||
availableMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster.name))]
|
availableMetrics.add({name: gm.name, scope: gm.scope, unit: gm.unit});
|
||||||
for (let sm of availableMetrics) {
|
};
|
||||||
metricUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
}
|
||||||
metricScopes[sm.name] = sm?.scope
|
}
|
||||||
|
return [...availableMetrics]
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadUnitsAndScopes(length, available) {
|
||||||
|
for (let am of available) {
|
||||||
|
metricUnits[am.name] = (am?.unit?.prefix ? am.unit.prefix : "") + (am?.unit?.base ? am.unit.base : "")
|
||||||
|
metricScopes[am.name] = am?.scope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: loadMetrics($initialized)
|
/* On Mount */
|
||||||
$: updateEntityConfiguration(groupSelection.key);
|
|
||||||
$: updateCategoryConfiguration(sortSelection.key);
|
|
||||||
|
|
||||||
onMount(() => filterComponent.updateFilters());
|
onMount(() => filterComponent.updateFilters());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -329,7 +357,7 @@
|
|||||||
{filterPresets}
|
{filterPresets}
|
||||||
disableClusterSelection={true}
|
disableClusterSelection={true}
|
||||||
startTimeQuickSelect={true}
|
startTimeQuickSelect={true}
|
||||||
on:update-filters={({ detail }) => {
|
applyFilters={(detail) => {
|
||||||
jobFilters = detail.filters;
|
jobFilters = detail.filters;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -392,6 +420,7 @@
|
|||||||
<Card body color="danger">{$topQuery.error.message}</Card>
|
<Card body color="danger">{$topQuery.error.message}</Card>
|
||||||
{:else}
|
{:else}
|
||||||
<Pie
|
<Pie
|
||||||
|
canvasId={`pie-${groupSelection.key}`}
|
||||||
size={colWidth1}
|
size={colWidth1}
|
||||||
sliceLabel={sortSelection.label}
|
sliceLabel={sortSelection.label}
|
||||||
quantities={$topQuery.data.topList.map(
|
quantities={$topQuery.data.topList.map(
|
||||||
|
|||||||
@@ -14,15 +14,18 @@
|
|||||||
import SupportSettings from "./config/SupportSettings.svelte";
|
import SupportSettings from "./config/SupportSettings.svelte";
|
||||||
import AdminSettings from "./config/AdminSettings.svelte";
|
import AdminSettings from "./config/AdminSettings.svelte";
|
||||||
|
|
||||||
export let isAdmin;
|
/* Svelte 5 Props */
|
||||||
export let isSupport;
|
let {
|
||||||
export let isApi;
|
isAdmin,
|
||||||
export let username;
|
isSupport,
|
||||||
export let ncontent;
|
isApi,
|
||||||
|
username,
|
||||||
|
ncontent,
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<Card style="margin-bottom: 1.5em;">
|
<Card style="margin-bottom: 1.5rem;">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-1">Admin Options</CardTitle>
|
<CardTitle class="mb-1">Admin Options</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isSupport || isAdmin}
|
{#if isSupport || isAdmin}
|
||||||
<Card style="margin-bottom: 1.5em;">
|
<Card style="margin-bottom: 1.5rem;">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="mb-1">Support Options</CardTitle>
|
<CardTitle class="mb-1">Support Options</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
- `username String`: Empty string if auth. is disabled, otherwise the username as string
|
- `username String`: Empty string if auth. is disabled, otherwise the username as string
|
||||||
- `authlevel Number`: The current users authentication level
|
- `authlevel Number`: The current users authentication level
|
||||||
- `clusters [String]`: List of cluster names
|
- `clusters [String]`: List of cluster names
|
||||||
|
- `subClusters [String]`: List of subCluster names
|
||||||
- `roles [Number]`: Enum containing available roles
|
- `roles [Number]`: Enum containing available roles
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@@ -23,24 +24,21 @@
|
|||||||
import NavbarLinks from "./header/NavbarLinks.svelte";
|
import NavbarLinks from "./header/NavbarLinks.svelte";
|
||||||
import NavbarTools from "./header/NavbarTools.svelte";
|
import NavbarTools from "./header/NavbarTools.svelte";
|
||||||
|
|
||||||
export let username;
|
/* Svelte 5 Props */
|
||||||
export let authlevel;
|
let { username, authlevel, clusters, subClusters, roles } = $props();
|
||||||
export let clusters;
|
|
||||||
export let subClusters;
|
|
||||||
export let roles;
|
|
||||||
|
|
||||||
let isOpen = false;
|
|
||||||
let screenSize;
|
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const jobsTitle = new Map();
|
const jobsTitle = new Map();
|
||||||
jobsTitle.set(2, "Job Search");
|
jobsTitle.set(2, "Job Search");
|
||||||
jobsTitle.set(3, "Managed Jobs");
|
jobsTitle.set(3, "Managed Jobs");
|
||||||
jobsTitle.set(4, "Jobs");
|
jobsTitle.set(4, "Jobs");
|
||||||
jobsTitle.set(5, "Jobs");
|
jobsTitle.set(5, "Jobs");
|
||||||
|
|
||||||
const usersTitle = new Map();
|
const usersTitle = new Map();
|
||||||
usersTitle.set(3, "Managed Users");
|
usersTitle.set(3, "Managed Users");
|
||||||
usersTitle.set(4, "Users");
|
usersTitle.set(4, "Users");
|
||||||
usersTitle.set(5, "Users");
|
usersTitle.set(5, "Users");
|
||||||
|
|
||||||
const projectsTitle = new Map();
|
const projectsTitle = new Map();
|
||||||
projectsTitle.set(3, "Managed Projects");
|
projectsTitle.set(3, "Managed Projects");
|
||||||
projectsTitle.set(4, "Projects");
|
projectsTitle.set(4, "Projects");
|
||||||
@@ -120,29 +118,41 @@
|
|||||||
menu: "Info",
|
menu: "Info",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let isOpen = $state(false);
|
||||||
|
let screenSize = $state(0);
|
||||||
|
|
||||||
|
/* Derived Vars */
|
||||||
|
let showMax = $derived(screenSize >= 1500);
|
||||||
|
let showMid = $derived(screenSize < 1500 && screenSize >= 1300);
|
||||||
|
let showSml = $derived(screenSize < 1300 && screenSize >= 768);
|
||||||
|
let showBrg = $derived(screenSize < 768);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerWidth={screenSize} />
|
<svelte:window bind:innerWidth={screenSize} />
|
||||||
|
|
||||||
<Navbar color="light" light expand="md" fixed="top">
|
<Navbar color="light" light expand="md" fixed="top">
|
||||||
<NavbarBrand href="/">
|
<NavbarBrand href="/">
|
||||||
<img alt="ClusterCockpit Logo" src="/img/logo.png" height="25rem" />
|
<img alt="ClusterCockpit Logo" src="/img/logo.png" height="25rem" />
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
|
<NavbarToggler onclick={() => (isOpen = !isOpen)} />
|
||||||
<Collapse
|
<Collapse
|
||||||
style="justify-content: space-between"
|
style="justify-content: space-between"
|
||||||
{isOpen}
|
{isOpen}
|
||||||
navbar
|
navbar
|
||||||
expand="md"
|
expand="md"
|
||||||
on:update={({ detail }) => (isOpen = detail.isOpen)}
|
onupdate={({ detail }) => (isOpen = detail.isOpen)}
|
||||||
>
|
>
|
||||||
<Nav navbar>
|
<Nav navbar>
|
||||||
{#if screenSize > 1500 || screenSize < 768}
|
{#if showMax || showBrg}
|
||||||
<NavbarLinks
|
<NavbarLinks
|
||||||
{clusters}
|
{clusters}
|
||||||
{subClusters}
|
{subClusters}
|
||||||
links={views.filter((item) => item.requiredRole <= authlevel)}
|
links={views.filter((item) => item.requiredRole <= authlevel)}
|
||||||
/>
|
/>
|
||||||
{:else if screenSize > 1300}
|
|
||||||
|
{:else if showMid}
|
||||||
<NavbarLinks
|
<NavbarLinks
|
||||||
{clusters}
|
{clusters}
|
||||||
{subClusters}
|
{subClusters}
|
||||||
@@ -169,7 +179,8 @@
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
|
||||||
|
{:else if showSml}
|
||||||
<NavbarLinks
|
<NavbarLinks
|
||||||
{clusters}
|
{clusters}
|
||||||
{subClusters}
|
{subClusters}
|
||||||
@@ -228,8 +239,11 @@
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<span>Error: Unknown Window Size!</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Nav>
|
</Nav>
|
||||||
<NavbarTools {username} {authlevel} {roles} {screenSize} />
|
<NavbarTools {username} {authlevel} {roles} {screenSize} />
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
selectedScopes = [],
|
selectedScopes = [],
|
||||||
plots = {};
|
plots = {};
|
||||||
|
|
||||||
let availableMetrics = new Set(),
|
let totalMetrics = 0;
|
||||||
missingMetrics = [],
|
let missingMetrics = [],
|
||||||
missingHosts = [],
|
missingHosts = [],
|
||||||
somethingMissing = false;
|
somethingMissing = false;
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
{#if $initq?.data}
|
{#if $initq?.data}
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary">
|
<Button outline on:click={() => (isMetricsSelectionOpen = true)} color="primary">
|
||||||
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
|
Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -428,12 +428,16 @@
|
|||||||
|
|
||||||
{#if $initq?.data}
|
{#if $initq?.data}
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
|
bind:isOpen={isMetricsSelectionOpen}
|
||||||
|
bind:totalMetrics
|
||||||
|
presetMetrics={selectedMetrics}
|
||||||
cluster={$initq.data.job.cluster}
|
cluster={$initq.data.job.cluster}
|
||||||
subCluster={$initq.data.job.subCluster}
|
subCluster={$initq.data.job.subCluster}
|
||||||
configName="job_view_selectedMetrics"
|
configName="job_view_selectedMetrics"
|
||||||
bind:metrics={selectedMetrics}
|
preInitialized
|
||||||
bind:isOpen={isMetricsSelectionOpen}
|
applyMetrics={(newMetrics) =>
|
||||||
bind:allMetrics={availableMetrics}
|
selectedMetrics = [...newMetrics]
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { onMount, getContext } from "svelte";
|
import { untrack, onMount, getContext } from "svelte";
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
@@ -27,43 +27,59 @@
|
|||||||
import Sorting from "./generic/select/SortSelection.svelte";
|
import Sorting from "./generic/select/SortSelection.svelte";
|
||||||
import MetricSelection from "./generic/select/MetricSelection.svelte";
|
import MetricSelection from "./generic/select/MetricSelection.svelte";
|
||||||
|
|
||||||
|
/* Svelte 5 Props */
|
||||||
|
let { filterPresets, authlevel, roles } = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
|
|
||||||
const ccconfig = getContext("cc-config");
|
const ccconfig = getContext("cc-config");
|
||||||
|
const presetProject = filterPresets?.project ? filterPresets.project : ""
|
||||||
|
|
||||||
export let filterPresets = {};
|
/* State Init */
|
||||||
export let authlevel;
|
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
|
||||||
export let roles;
|
let selectedJobs = $state([]);
|
||||||
|
let filterBuffer = $state([]);
|
||||||
let filterComponent; // 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 jobList = $state(null);
|
||||||
let filterBuffer = [];
|
let jobCompare = $state(null);
|
||||||
let selectedJobs = [];
|
let matchedListJobs = $state(0);
|
||||||
let jobList,
|
let matchedCompareJobs = $state(0);
|
||||||
jobCompare,
|
let isSortingOpen = $state(false);
|
||||||
matchedListJobs,
|
let showCompare = $state(false);
|
||||||
matchedCompareJobs = null;
|
let isMetricsSelectionOpen = $state(false);
|
||||||
let sorting = { field: "startTime", type: "col", order: "DESC" },
|
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
||||||
isSortingOpen = false,
|
let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
|
||||||
isMetricsSelectionOpen = false;
|
let metrics = $state(filterPresets.cluster
|
||||||
let metrics = filterPresets.cluster
|
|
||||||
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
|
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
|
||||||
ccconfig.plot_list_selectedMetrics
|
ccconfig.plot_list_selectedMetrics
|
||||||
: ccconfig.plot_list_selectedMetrics;
|
: ccconfig.plot_list_selectedMetrics
|
||||||
let showFootprint = filterPresets.cluster
|
);
|
||||||
|
let showFootprint = $state(filterPresets.cluster
|
||||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||||
: !!ccconfig.plot_list_showFootprint;
|
: !!ccconfig.plot_list_showFootprint
|
||||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
);
|
||||||
let presetProject = filterPresets?.project ? filterPresets.project : ""
|
|
||||||
let showCompare = false;
|
|
||||||
|
|
||||||
|
/* 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()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* On Mount */
|
||||||
// The filterPresets are handled by the Filters component,
|
// The filterPresets are handled by the Filters component,
|
||||||
// so we need to wait for it to be ready before we can start a query.
|
// so we need to wait for it to be ready before we can start a query.
|
||||||
// This is also why JobList component starts out with a paused query.
|
// This is also why JobList component starts out with a paused query.
|
||||||
onMount(() => filterComponent.updateFilters());
|
onMount(() => filterComponent.updateFilters());
|
||||||
|
|
||||||
$: if (filterComponent && selectedJobs.length == 0) {
|
|
||||||
filterComponent.updateFilters({dbId: []})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- ROW1: Status-->
|
<!-- ROW1: Status-->
|
||||||
@@ -85,25 +101,25 @@
|
|||||||
<Row cols={{ xs: 1, md: 2, lg: 5}} class="mb-3">
|
<Row cols={{ xs: 1, md: 2, lg: 5}} class="mb-3">
|
||||||
<Col lg="2" class="mb-2 mb-lg-0">
|
<Col lg="2" class="mb-2 mb-lg-0">
|
||||||
<ButtonGroup class="w-100">
|
<ButtonGroup class="w-100">
|
||||||
<Button outline color="primary" on:click={() => (isSortingOpen = true)} disabled={showCompare}>
|
<Button outline color="primary" onclick={() => (isSortingOpen = true)} disabled={showCompare}>
|
||||||
<Icon name="sort-up" /> Sorting
|
<Icon name="sort-up" /> Sorting
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => (isMetricsSelectionOpen = true)}
|
onclick={() => (isMetricsSelectionOpen = true)}
|
||||||
>
|
>
|
||||||
<Icon name="graph-up" /> Metrics
|
<Icon name="graph-up" /> Metrics
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg="4" class="mb-1 mb-lg-0">
|
<Col lg="5" class="mb-1 mb-lg-0">
|
||||||
<Filters
|
<Filters
|
||||||
showFilter={!showCompare}
|
|
||||||
{filterPresets}
|
|
||||||
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
|
|
||||||
bind:this={filterComponent}
|
bind:this={filterComponent}
|
||||||
on:update-filters={({ detail }) => {
|
{filterPresets}
|
||||||
|
showFilter={!showCompare}
|
||||||
|
matchedJobs={showCompare? matchedCompareJobs: matchedListJobs}
|
||||||
|
applyFilters={(detail) => {
|
||||||
selectedCluster = detail.filters[0]?.cluster
|
selectedCluster = detail.filters[0]?.cluster
|
||||||
? detail.filters[0].cluster.eq
|
? detail.filters[0].cluster.eq
|
||||||
: null;
|
: null;
|
||||||
@@ -122,29 +138,31 @@
|
|||||||
{presetProject}
|
{presetProject}
|
||||||
bind:authlevel
|
bind:authlevel
|
||||||
bind:roles
|
bind:roles
|
||||||
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
|
setFilter={(filter) => filterComponent.updateFilters(filter)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg="2" class="mb-1 mb-lg-0">
|
<Col lg="3" class="mb-1 mb-lg-0 d-inline-flex align-items-start justify-content-end ">
|
||||||
{#if !showCompare}
|
{#if !showCompare}
|
||||||
<Refresher on:refresh={() => {
|
<Refresher presetClass="w-auto" onRefresh={() => {
|
||||||
jobList.refreshJobs()
|
jobList.refreshJobs()
|
||||||
jobList.refreshAllMetrics()
|
jobList.refreshAllMetrics()
|
||||||
}} />
|
}} />
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
<div class="mx-1"></div>
|
||||||
<Col lg="2" class="mb-2 mb-lg-0">
|
<ButtonGroup class="w-50">
|
||||||
<ButtonGroup class="w-100">
|
<Button color="primary" disabled={(matchedListJobs >= 500 && !(selectedJobs.length != 0)) || $initq.fetching} onclick={() => {
|
||||||
<Button color="primary" disabled={matchedListJobs >= 500 && !(selectedJobs.length != 0)} on:click={() => {
|
|
||||||
if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs}, true)
|
if (selectedJobs.length != 0) filterComponent.updateFilters({dbId: selectedJobs}, true)
|
||||||
showCompare = !showCompare
|
showCompare = !showCompare
|
||||||
}} >
|
}} >
|
||||||
{showCompare ? 'Return to List' :
|
{showCompare ? 'Return to List' :
|
||||||
'Compare Jobs' + (selectedJobs.length != 0 ? ` (${selectedJobs.length} selected)` : matchedListJobs >= 500 ? ` (Too Many)` : ``)}
|
matchedListJobs >= 500 && selectedJobs.length == 0
|
||||||
|
? 'Compare Disabled'
|
||||||
|
: 'Compare' + (selectedJobs.length != 0 ? ` ${selectedJobs.length} ` : ' ') + 'Jobs'
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
{#if !showCompare && selectedJobs.length != 0}
|
{#if !showCompare && selectedJobs.length != 0}
|
||||||
<Button color="warning" on:click={() => {
|
<Button class="w-auto" color="warning" onclick={() => {
|
||||||
selectedJobs = [] // Only empty array, filters handled by reactive reset
|
selectedJobs = [] // Only empty array, filters handled by reactive reset
|
||||||
}}>
|
}}>
|
||||||
Clear
|
Clear
|
||||||
@@ -178,13 +196,21 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Sorting bind:sorting bind:isOpen={isSortingOpen}/>
|
<Sorting
|
||||||
|
bind:isOpen={isSortingOpen}
|
||||||
|
presetSorting={sorting}
|
||||||
|
applySorting={(newSort) =>
|
||||||
|
sorting = {...newSort}
|
||||||
|
}/>
|
||||||
|
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
bind:cluster={selectedCluster}
|
bind:isOpen={isMetricsSelectionOpen}
|
||||||
configName="plot_list_selectedMetrics"
|
bind:showFootprint
|
||||||
bind:metrics
|
presetMetrics={metrics}
|
||||||
bind:isOpen={isMetricsSelectionOpen}
|
cluster={selectedCluster}
|
||||||
bind:showFootprint
|
configName="plot_list_selectedMetrics"
|
||||||
footprintSelect
|
footprintSelect
|
||||||
|
applyMetrics={(newMetrics) =>
|
||||||
|
metrics = [...newMetrics]
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -31,10 +31,8 @@
|
|||||||
} from "./generic/utils.js";
|
} from "./generic/utils.js";
|
||||||
import Filters from "./generic/Filters.svelte";
|
import Filters from "./generic/Filters.svelte";
|
||||||
|
|
||||||
const {} = init();
|
/* Svelte 5 Props */
|
||||||
|
let { type, filterPresets } = $props();
|
||||||
export let type;
|
|
||||||
export let filterPresets;
|
|
||||||
|
|
||||||
// By default, look at the jobs of the last 30 days:
|
// By default, look at the jobs of the last 30 days:
|
||||||
if (filterPresets?.startTime == null) {
|
if (filterPresets?.startTime == null) {
|
||||||
@@ -51,50 +49,64 @@
|
|||||||
"Invalid list type provided!",
|
"Invalid list type provided!",
|
||||||
);
|
);
|
||||||
|
|
||||||
let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
|
/* Const Init */
|
||||||
let jobFilters = [];
|
const {} = init();
|
||||||
let nameFilter = "";
|
|
||||||
let sorting = { field: "totalJobs", direction: "down" };
|
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
$: stats = queryStore({
|
|
||||||
client: client,
|
|
||||||
query: gql`
|
|
||||||
query($jobFilters: [JobFilter!]!) {
|
|
||||||
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
totalJobs
|
|
||||||
totalWalltime
|
|
||||||
totalCoreHours
|
|
||||||
totalAccHours
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
variables: { jobFilters },
|
|
||||||
});
|
|
||||||
|
|
||||||
function changeSorting(event, field) {
|
/* State Init*/
|
||||||
let target = event.target;
|
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
|
||||||
while (target.tagName != "BUTTON") target = target.parentElement;
|
let jobFilters = $state([]);
|
||||||
|
let nameFilter = $state("");
|
||||||
|
let sorting = $state({ field: "totalJobs", direction: "down" });
|
||||||
|
|
||||||
let direction = target.children[0].className.includes("up") ? "down" : "up";
|
/* Derived Vars */
|
||||||
target.children[0].className = `bi-sort-numeric-${direction}`;
|
let stats = $derived(
|
||||||
sorting = { field, direction };
|
queryStore({
|
||||||
|
client: client,
|
||||||
|
query: gql`
|
||||||
|
query($jobFilters: [JobFilter!]!) {
|
||||||
|
rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
totalJobs
|
||||||
|
totalWalltime
|
||||||
|
totalCoreHours
|
||||||
|
totalAccHours
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
variables: { jobFilters },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function changeSorting(field) {
|
||||||
|
sorting = { field, direction: sorting?.direction == "down" ? "up" : "down" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function sort(stats, sorting, nameFilter) {
|
function sort(stats, sorting, nameFilter) {
|
||||||
const cmp =
|
const idCmp = sorting.direction == "up"
|
||||||
sorting.field == "id"
|
? (a, b) => b.id.localeCompare(a.id)
|
||||||
? sorting.direction == "up"
|
: (a, b) => a.id.localeCompare(b.id)
|
||||||
? (a, b) => a.id < b.id
|
|
||||||
: (a, b) => a.id > b.id
|
|
||||||
: sorting.direction == "up"
|
|
||||||
? (a, b) => a[sorting.field] - b[sorting.field]
|
|
||||||
: (a, b) => b[sorting.field] - a[sorting.field];
|
|
||||||
|
|
||||||
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
|
// Force empty or undefined strings to the end of the list
|
||||||
|
const nameCmp = sorting.direction == "up"
|
||||||
|
? (a, b) => !a?.name ? 1 : (!b?.name ? -1 : (b.name.localeCompare(a.name)))
|
||||||
|
: (a, b) => !a?.name ? 1 : (!b?.name ? -1 : (a.name.localeCompare(b.name)))
|
||||||
|
|
||||||
|
const intCmp = sorting.direction == "up"
|
||||||
|
? (a, b) => a[sorting.field] - b[sorting.field]
|
||||||
|
: (a, b) => b[sorting.field] - a[sorting.field];
|
||||||
|
|
||||||
|
if (sorting.field == "id") {
|
||||||
|
return stats.filter((u) => u.id.includes(nameFilter)).sort(idCmp)
|
||||||
|
} else if (sorting.field == "name") {
|
||||||
|
return stats.filter((u) => u.id.includes(nameFilter)).sort(nameCmp)
|
||||||
|
} else {
|
||||||
|
return stats.filter((u) => u.id.includes(nameFilter)).sort(intCmp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On Mount */
|
||||||
onMount(() => filterComponent.updateFilters());
|
onMount(() => filterComponent.updateFilters());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -119,7 +131,7 @@
|
|||||||
{filterPresets}
|
{filterPresets}
|
||||||
startTimeQuickSelect={true}
|
startTimeQuickSelect={true}
|
||||||
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
|
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
|
||||||
on:update-filters={({ detail }) => {
|
applyFilters={(detail) => {
|
||||||
jobFilters = detail.filters;
|
jobFilters = detail.filters;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -137,9 +149,14 @@
|
|||||||
<Button
|
<Button
|
||||||
color={sorting.field == "id" ? "primary" : "light"}
|
color={sorting.field == "id" ? "primary" : "light"}
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "id")}
|
onclick={() => changeSorting("id")}
|
||||||
>
|
>
|
||||||
<Icon name="sort-numeric-down" />
|
{#if sorting?.field == "id"}
|
||||||
|
<!-- Note on Icon-Name: Arrow-indicator always down, only alpha-indicator switches -->
|
||||||
|
<Icon name={`sort-alpha-${sorting?.direction == 'down' ? 'down' : 'down-alt'}`} />
|
||||||
|
{:else}
|
||||||
|
<Icon name="three-dots-vertical" />
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
{#if type == "USER"}
|
{#if type == "USER"}
|
||||||
@@ -148,9 +165,13 @@
|
|||||||
<Button
|
<Button
|
||||||
color={sorting.field == "name" ? "primary" : "light"}
|
color={sorting.field == "name" ? "primary" : "light"}
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "name")}
|
onclick={() => changeSorting("name")}
|
||||||
>
|
>
|
||||||
<Icon name="sort-numeric-down" />
|
{#if sorting?.field == "name"}
|
||||||
|
<Icon name={`sort-alpha-${sorting?.direction == 'down' ? 'down' : 'down-alt'}`} />
|
||||||
|
{:else}
|
||||||
|
<Icon name="three-dots-vertical" />
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -159,9 +180,14 @@
|
|||||||
<Button
|
<Button
|
||||||
color={sorting.field == "totalJobs" ? "primary" : "light"}
|
color={sorting.field == "totalJobs" ? "primary" : "light"}
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "totalJobs")}
|
onclick={() => changeSorting("totalJobs")}
|
||||||
>
|
>
|
||||||
<Icon name="sort-numeric-down" />
|
{#if sorting?.field == "totalJobs"}
|
||||||
|
<!-- Note on Icon-Name: Arrow-indicator always down, only numeric-indicator switches -->
|
||||||
|
<Icon name={`sort-numeric-${sorting?.direction == 'down' ? 'down-alt' : 'down'}`} />
|
||||||
|
{:else}
|
||||||
|
<Icon name="three-dots-vertical" />
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
@@ -169,9 +195,13 @@
|
|||||||
<Button
|
<Button
|
||||||
color={sorting.field == "totalWalltime" ? "primary" : "light"}
|
color={sorting.field == "totalWalltime" ? "primary" : "light"}
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "totalWalltime")}
|
onclick={() => changeSorting("totalWalltime")}
|
||||||
>
|
>
|
||||||
<Icon name="sort-numeric-down" />
|
{#if sorting?.field == "totalWalltime"}
|
||||||
|
<Icon name={`sort-numeric-${sorting?.direction == 'down' ? 'down-alt' : 'down'}`} />
|
||||||
|
{:else}
|
||||||
|
<Icon name="three-dots-vertical" />
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
@@ -179,9 +209,13 @@
|
|||||||
<Button
|
<Button
|
||||||
color={sorting.field == "totalCoreHours" ? "primary" : "light"}
|
color={sorting.field == "totalCoreHours" ? "primary" : "light"}
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "totalCoreHours")}
|
onclick={() => changeSorting("totalCoreHours")}
|
||||||
>
|
>
|
||||||
<Icon name="sort-numeric-down" />
|
{#if sorting?.field == "totalCoreHours"}
|
||||||
|
<Icon name={`sort-numeric-${sorting?.direction == 'down' ? 'down-alt' : 'down'}`} />
|
||||||
|
{:else}
|
||||||
|
<Icon name="three-dots-vertical" />
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
@@ -189,9 +223,13 @@
|
|||||||
<Button
|
<Button
|
||||||
color={sorting.field == "totalAccHours" ? "primary" : "light"}
|
color={sorting.field == "totalAccHours" ? "primary" : "light"}
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={(e) => changeSorting(e, "totalAccHours")}
|
onclick={() => changeSorting("totalAccHours")}
|
||||||
>
|
>
|
||||||
<Icon name="sort-numeric-down" />
|
{#if sorting?.field == "totalAccHours"}
|
||||||
|
<Icon name={`sort-numeric-${sorting?.direction == 'down' ? 'down-alt' : 'down'}`} />
|
||||||
|
{:else}
|
||||||
|
<Icon name="three-dots-vertical" />
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import {
|
||||||
|
getContext,
|
||||||
|
} from "svelte";
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
@@ -34,23 +36,28 @@
|
|||||||
import TimeSelection from "./generic/select/TimeSelection.svelte";
|
import TimeSelection from "./generic/select/TimeSelection.svelte";
|
||||||
import Refresher from "./generic/helper/Refresher.svelte";
|
import Refresher from "./generic/helper/Refresher.svelte";
|
||||||
|
|
||||||
export let cluster;
|
/* Svelte 5 Props */
|
||||||
export let hostname;
|
let {
|
||||||
export let from = null;
|
cluster,
|
||||||
export let to = null;
|
hostname,
|
||||||
|
presetFrom = null,
|
||||||
|
presetTo = null,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
|
|
||||||
if (from == null || to == null) {
|
|
||||||
to = new Date(Date.now());
|
|
||||||
from = new Date(to.getTime());
|
|
||||||
from.setHours(from.getHours() - 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialized = getContext("initialized")
|
const initialized = getContext("initialized")
|
||||||
const globalMetrics = getContext("globalMetrics")
|
const globalMetrics = getContext("globalMetrics")
|
||||||
const ccconfig = getContext("cc-config");
|
const ccconfig = getContext("cc-config");
|
||||||
const clusters = getContext("clusters");
|
const clusters = getContext("clusters");
|
||||||
|
const nowEpoch = Date.now();
|
||||||
|
const paging = { itemsPerPage: 50, page: 1 };
|
||||||
|
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
||||||
|
const filter = [
|
||||||
|
{ cluster: { eq: cluster } },
|
||||||
|
{ node: { contains: hostname } },
|
||||||
|
{ state: ["running"] },
|
||||||
|
];
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const nodeMetricsQuery = gql`
|
const nodeMetricsQuery = gql`
|
||||||
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
|
query ($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
|
||||||
@@ -79,27 +86,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
$: nodeMetricsData = queryStore({
|
|
||||||
client: client,
|
|
||||||
query: nodeMetricsQuery,
|
|
||||||
variables: {
|
|
||||||
cluster: cluster,
|
|
||||||
nodes: [hostname],
|
|
||||||
from: from.toISOString(),
|
|
||||||
to: to.toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const paging = { itemsPerPage: 50, page: 1 };
|
|
||||||
const sorting = { field: "startTime", type: "col", order: "DESC" };
|
|
||||||
const filter = [
|
|
||||||
{ cluster: { eq: cluster } },
|
|
||||||
{ node: { contains: hostname } },
|
|
||||||
{ state: ["running"] },
|
|
||||||
];
|
|
||||||
|
|
||||||
const nodeJobsQuery = gql`
|
const nodeJobsQuery = gql`
|
||||||
query (
|
query (
|
||||||
$filter: [JobFilter!]!
|
$filter: [JobFilter!]!
|
||||||
@@ -112,13 +98,37 @@
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
$: nodeJobsData = queryStore({
|
/* State Init */
|
||||||
client: client,
|
let from = $state(presetFrom ? presetFrom : new Date(nowEpoch - (4 * 3600 * 1000)));
|
||||||
query: nodeJobsQuery,
|
let to = $state(presetTo ? presetTo : new Date(nowEpoch));
|
||||||
variables: { paging, sorting, filter },
|
let systemUnits = $state({});
|
||||||
|
|
||||||
|
/* Derived */
|
||||||
|
const nodeMetricsData = $derived(queryStore({
|
||||||
|
client: client,
|
||||||
|
query: nodeMetricsQuery,
|
||||||
|
variables: {
|
||||||
|
cluster: cluster,
|
||||||
|
nodes: [hostname],
|
||||||
|
from: from?.toISOString(),
|
||||||
|
to: to?.toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeJobsData = $derived(queryStore({
|
||||||
|
client: client,
|
||||||
|
query: nodeJobsQuery,
|
||||||
|
variables: { paging, sorting, filter },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Effect */
|
||||||
|
$effect(() => {
|
||||||
|
loadUnits($initialized);
|
||||||
});
|
});
|
||||||
|
|
||||||
let systemUnits = {};
|
/* Functions */
|
||||||
function loadUnits(isInitialized) {
|
function loadUnits(isInitialized) {
|
||||||
if (!isInitialized) return
|
if (!isInitialized) return
|
||||||
const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
|
const systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
|
||||||
@@ -126,8 +136,6 @@
|
|||||||
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: loadUnits($initialized)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={{ xs: 2, lg: 4 }}>
|
<Row cols={{ xs: 2, lg: 4 }}>
|
||||||
@@ -146,7 +154,14 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<!-- Time Col -->
|
<!-- Time Col -->
|
||||||
<Col>
|
<Col>
|
||||||
<TimeSelection bind:from bind:to />
|
<TimeSelection
|
||||||
|
presetFrom={from}
|
||||||
|
presetTo={to}
|
||||||
|
applyTime={(newFrom, newTo) => {
|
||||||
|
from = newFrom;
|
||||||
|
to = newTo;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<!-- Concurrent Col -->
|
<!-- Concurrent Col -->
|
||||||
<Col class="mt-2 mt-lg-0">
|
<Col class="mt-2 mt-lg-0">
|
||||||
@@ -172,7 +187,7 @@
|
|||||||
<!-- Refresh Col-->
|
<!-- Refresh Col-->
|
||||||
<Col class="mt-2 mt-lg-0">
|
<Col class="mt-2 mt-lg-0">
|
||||||
<Refresher
|
<Refresher
|
||||||
on:refresh={() => {
|
onRefresh={() => {
|
||||||
const diff = Date.now() - to;
|
const diff = Date.now() - to;
|
||||||
from = new Date(from.getTime() + diff);
|
from = new Date(from.getTime() + diff);
|
||||||
to = new Date(to.getTime() + diff);
|
to = new Date(to.getTime() + diff);
|
||||||
|
|||||||
@@ -42,15 +42,14 @@
|
|||||||
import Refresher from "./generic/helper/Refresher.svelte";
|
import Refresher from "./generic/helper/Refresher.svelte";
|
||||||
import HistogramSelection from "./generic/select/HistogramSelection.svelte";
|
import HistogramSelection from "./generic/select/HistogramSelection.svelte";
|
||||||
|
|
||||||
|
/* Svelte 5 Props */
|
||||||
|
let { cluster } = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
const ccconfig = getContext("cc-config");
|
const ccconfig = getContext("cc-config");
|
||||||
|
const client = getContextClient();
|
||||||
export let cluster;
|
const paging = { itemsPerPage: 10, page: 1 }; // Top 10
|
||||||
|
|
||||||
let plotWidths = [];
|
|
||||||
let colWidth;
|
|
||||||
let from = new Date(Date.now() - 5 * 60 * 1000),
|
|
||||||
to = new Date(Date.now());
|
|
||||||
const topOptions = [
|
const topOptions = [
|
||||||
{ key: "totalJobs", label: "Jobs" },
|
{ key: "totalJobs", label: "Jobs" },
|
||||||
{ key: "totalNodes", label: "Nodes" },
|
{ key: "totalNodes", label: "Nodes" },
|
||||||
@@ -58,7 +57,26 @@
|
|||||||
{ key: "totalAccs", label: "Accelerators" },
|
{ key: "totalAccs", label: "Accelerators" },
|
||||||
];
|
];
|
||||||
|
|
||||||
let topProjectSelection =
|
/* State Init */
|
||||||
|
let from = $state(new Date(Date.now() - 5 * 60 * 1000));
|
||||||
|
let to = $state(new Date(Date.now()));
|
||||||
|
let isHistogramSelectionOpen = $state(false);
|
||||||
|
let colWidth = $state(0);
|
||||||
|
let plotWidths = $state([]);
|
||||||
|
// Bar Gauges
|
||||||
|
let allocatedNodes = $state({});
|
||||||
|
let flopRate = $state({});
|
||||||
|
let flopRateUnitPrefix = $state({});
|
||||||
|
let flopRateUnitBase = $state({});
|
||||||
|
let memBwRate = $state({});
|
||||||
|
let memBwRateUnitPrefix = $state({});
|
||||||
|
let memBwRateUnitBase = $state({});
|
||||||
|
|
||||||
|
let selectedHistograms = $state(cluster
|
||||||
|
? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
|
||||||
|
: ccconfig['user_view_histogramMetrics'] || []);
|
||||||
|
|
||||||
|
let topProjectSelection = $state(
|
||||||
topOptions.find(
|
topOptions.find(
|
||||||
(option) =>
|
(option) =>
|
||||||
option.key ==
|
option.key ==
|
||||||
@@ -66,8 +84,10 @@
|
|||||||
) ||
|
) ||
|
||||||
topOptions.find(
|
topOptions.find(
|
||||||
(option) => option.key == ccconfig.status_view_selectedTopProjectCategory,
|
(option) => option.key == ccconfig.status_view_selectedTopProjectCategory,
|
||||||
);
|
)
|
||||||
let topUserSelection =
|
);
|
||||||
|
|
||||||
|
let topUserSelection = $state(
|
||||||
topOptions.find(
|
topOptions.find(
|
||||||
(option) =>
|
(option) =>
|
||||||
option.key ==
|
option.key ==
|
||||||
@@ -75,16 +95,12 @@
|
|||||||
) ||
|
) ||
|
||||||
topOptions.find(
|
topOptions.find(
|
||||||
(option) => option.key == ccconfig.status_view_selectedTopUserCategory,
|
(option) => option.key == ccconfig.status_view_selectedTopUserCategory,
|
||||||
);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
let isHistogramSelectionOpen = false;
|
/* Derived */
|
||||||
$: selectedHistograms = cluster
|
|
||||||
? ccconfig[`user_view_histogramMetrics:${cluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
|
|
||||||
: ccconfig['user_view_histogramMetrics'] || [];
|
|
||||||
|
|
||||||
const client = getContextClient();
|
|
||||||
// Note: nodeMetrics are requested on configured $timestep resolution
|
// Note: nodeMetrics are requested on configured $timestep resolution
|
||||||
$: mainQuery = queryStore({
|
const mainQuery = $derived(queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query (
|
query (
|
||||||
@@ -162,10 +178,9 @@
|
|||||||
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
filter: [{ state: ["running"] }, { cluster: { eq: cluster } }],
|
||||||
selectedHistograms: selectedHistograms,
|
selectedHistograms: selectedHistograms,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
const paging = { itemsPerPage: 10, page: 1 }; // Top 10
|
const topUserQuery = $derived(queryStore({
|
||||||
$: topUserQuery = queryStore({
|
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query (
|
query (
|
||||||
@@ -193,9 +208,9 @@
|
|||||||
paging,
|
paging,
|
||||||
sortBy: topUserSelection.key.toUpperCase(),
|
sortBy: topUserSelection.key.toUpperCase(),
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
$: topProjectQuery = queryStore({
|
const topProjectQuery = $derived(queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: gql`
|
query: gql`
|
||||||
query (
|
query (
|
||||||
@@ -222,8 +237,46 @@
|
|||||||
paging,
|
paging,
|
||||||
sortBy: topProjectSelection.key.toUpperCase(),
|
sortBy: topProjectSelection.key.toUpperCase(),
|
||||||
},
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
$effect(() => {
|
||||||
|
if ($initq.data && $mainQuery.data) {
|
||||||
|
let subClusters = $initq.data.clusters.find(
|
||||||
|
(c) => c.name == cluster,
|
||||||
|
).subClusters;
|
||||||
|
for (let subCluster of subClusters) {
|
||||||
|
allocatedNodes[subCluster.name] =
|
||||||
|
$mainQuery.data.allocatedNodes.find(
|
||||||
|
({ name }) => name == subCluster.name,
|
||||||
|
)?.count || 0;
|
||||||
|
flopRate[subCluster.name] =
|
||||||
|
Math.floor(
|
||||||
|
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "flops_any") *
|
||||||
|
100,
|
||||||
|
) / 100;
|
||||||
|
flopRateUnitPrefix[subCluster.name] = subCluster.flopRateSimd.unit.prefix;
|
||||||
|
flopRateUnitBase[subCluster.name] = subCluster.flopRateSimd.unit.base;
|
||||||
|
memBwRate[subCluster.name] =
|
||||||
|
Math.floor(
|
||||||
|
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "mem_bw") * 100,
|
||||||
|
) / 100;
|
||||||
|
memBwRateUnitPrefix[subCluster.name] =
|
||||||
|
subCluster.memoryBandwidth.unit.prefix;
|
||||||
|
memBwRateUnitBase[subCluster.name] = subCluster.memoryBandwidth.unit.base;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
updateTopUserConfiguration(topUserSelection.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
updateTopProjectConfiguration(topProjectSelection.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Const Functions */
|
||||||
const sumUp = (data, subcluster, metric) =>
|
const sumUp = (data, subcluster, metric) =>
|
||||||
data.reduce(
|
data.reduce(
|
||||||
(sum, node) =>
|
(sum, node) =>
|
||||||
@@ -239,39 +292,6 @@
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let allocatedNodes = {},
|
|
||||||
flopRate = {},
|
|
||||||
flopRateUnitPrefix = {},
|
|
||||||
flopRateUnitBase = {},
|
|
||||||
memBwRate = {},
|
|
||||||
memBwRateUnitPrefix = {},
|
|
||||||
memBwRateUnitBase = {};
|
|
||||||
$: if ($initq.data && $mainQuery.data) {
|
|
||||||
let subClusters = $initq.data.clusters.find(
|
|
||||||
(c) => c.name == cluster,
|
|
||||||
).subClusters;
|
|
||||||
for (let subCluster of subClusters) {
|
|
||||||
allocatedNodes[subCluster.name] =
|
|
||||||
$mainQuery.data.allocatedNodes.find(
|
|
||||||
({ name }) => name == subCluster.name,
|
|
||||||
)?.count || 0;
|
|
||||||
flopRate[subCluster.name] =
|
|
||||||
Math.floor(
|
|
||||||
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "flops_any") *
|
|
||||||
100,
|
|
||||||
) / 100;
|
|
||||||
flopRateUnitPrefix[subCluster.name] = subCluster.flopRateSimd.unit.prefix;
|
|
||||||
flopRateUnitBase[subCluster.name] = subCluster.flopRateSimd.unit.base;
|
|
||||||
memBwRate[subCluster.name] =
|
|
||||||
Math.floor(
|
|
||||||
sumUp($mainQuery.data.nodeMetrics, subCluster.name, "mem_bw") * 100,
|
|
||||||
) / 100;
|
|
||||||
memBwRateUnitPrefix[subCluster.name] =
|
|
||||||
subCluster.memoryBandwidth.unit.prefix;
|
|
||||||
memBwRateUnitBase[subCluster.name] = subCluster.memoryBandwidth.unit.base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
@@ -284,20 +304,17 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function updateTopUserConfiguration(select) {
|
function updateTopUserConfiguration(select) {
|
||||||
if (ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select) {
|
if (ccconfig[`status_view_selectedTopUserCategory:${cluster}`] != select) {
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: `status_view_selectedTopUserCategory:${cluster}`,
|
name: `status_view_selectedTopUserCategory:${cluster}`,
|
||||||
value: JSON.stringify(select),
|
value: JSON.stringify(select),
|
||||||
}).subscribe((res) => {
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && !res.error) {
|
if (res.fetching === false && res.error) {
|
||||||
// console.log(`status_view_selectedTopUserCategory:${cluster}` + ' -> Updated!')
|
|
||||||
} else if (res.fetching === false && res.error) {
|
|
||||||
throw res.error;
|
throw res.error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// console.log('No Mutation Required: Top User')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,19 +326,12 @@
|
|||||||
name: `status_view_selectedTopProjectCategory:${cluster}`,
|
name: `status_view_selectedTopProjectCategory:${cluster}`,
|
||||||
value: JSON.stringify(select),
|
value: JSON.stringify(select),
|
||||||
}).subscribe((res) => {
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && !res.error) {
|
if (res.fetching === false && res.error) {
|
||||||
// console.log(`status_view_selectedTopProjectCategory:${cluster}` + ' -> Updated!')
|
|
||||||
} else if (res.fetching === false && res.error) {
|
|
||||||
throw res.error;
|
throw res.error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// console.log('No Mutation Required: Top Project')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: updateTopUserConfiguration(topUserSelection.key);
|
|
||||||
$: updateTopProjectConfiguration(topProjectSelection.key);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Loading indicator & Refresh -->
|
<!-- Loading indicator & Refresh -->
|
||||||
@@ -334,7 +344,7 @@
|
|||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (isHistogramSelectionOpen = true)}
|
onclick={() => (isHistogramSelectionOpen = true)}
|
||||||
>
|
>
|
||||||
<Icon name="bar-chart-line" /> Select Histograms
|
<Icon name="bar-chart-line" /> Select Histograms
|
||||||
</Button>
|
</Button>
|
||||||
@@ -342,7 +352,7 @@
|
|||||||
<Col class="mt-2 mt-md-0">
|
<Col class="mt-2 mt-md-0">
|
||||||
<Refresher
|
<Refresher
|
||||||
initially={120}
|
initially={120}
|
||||||
on:refresh={() => {
|
onRefresh={() => {
|
||||||
from = new Date(Date.now() - 5 * 60 * 1000);
|
from = new Date(Date.now() - 5 * 60 * 1000);
|
||||||
to = new Date(Date.now());
|
to = new Date(Date.now());
|
||||||
}}
|
}}
|
||||||
@@ -483,6 +493,7 @@
|
|||||||
<Card body color="danger">{$topUserQuery.error.message}</Card>
|
<Card body color="danger">{$topUserQuery.error.message}</Card>
|
||||||
{:else}
|
{:else}
|
||||||
<Pie
|
<Pie
|
||||||
|
canvasId="hpcpie-users"
|
||||||
size={colWidth}
|
size={colWidth}
|
||||||
sliceLabel={topUserSelection.label}
|
sliceLabel={topUserSelection.label}
|
||||||
quantities={$topUserQuery.data.topUser.map(
|
quantities={$topUserQuery.data.topUser.map(
|
||||||
@@ -550,6 +561,7 @@
|
|||||||
<Card body color="danger">{$topProjectQuery.error.message}</Card>
|
<Card body color="danger">{$topProjectQuery.error.message}</Card>
|
||||||
{:else}
|
{:else}
|
||||||
<Pie
|
<Pie
|
||||||
|
canvasId="hpcpie-projects"
|
||||||
size={colWidth}
|
size={colWidth}
|
||||||
sliceLabel={topProjectSelection.label}
|
sliceLabel={topProjectSelection.label}
|
||||||
quantities={$topProjectQuery.data.topProjects.map(
|
quantities={$topProjectQuery.data.topProjects.map(
|
||||||
@@ -685,7 +697,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<HistogramSelection
|
<HistogramSelection
|
||||||
bind:cluster
|
{cluster}
|
||||||
bind:selectedHistograms
|
|
||||||
bind:isOpen={isHistogramSelectionOpen}
|
bind:isOpen={isHistogramSelectionOpen}
|
||||||
|
presetSelectedHistograms={selectedHistograms}
|
||||||
|
applyChange={(newSelection) => {
|
||||||
|
selectedHistograms = [...newSelection];
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,72 +28,74 @@
|
|||||||
import TimeSelection from "./generic/select/TimeSelection.svelte";
|
import TimeSelection from "./generic/select/TimeSelection.svelte";
|
||||||
import Refresher from "./generic/helper/Refresher.svelte";
|
import Refresher from "./generic/helper/Refresher.svelte";
|
||||||
|
|
||||||
export let displayType;
|
|
||||||
export let cluster = null;
|
|
||||||
export let subCluster = null;
|
|
||||||
export let from = null;
|
|
||||||
export let to = null;
|
|
||||||
|
|
||||||
const { query: initq } = init();
|
|
||||||
|
|
||||||
console.assert(
|
|
||||||
displayType == "OVERVIEW" || displayType == "LIST",
|
|
||||||
"Invalid nodes displayType provided!",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (from == null || to == null) {
|
|
||||||
to = new Date(Date.now());
|
|
||||||
from = new Date(to.getTime());
|
|
||||||
from.setHours(from.getHours() - 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialized = getContext("initialized");
|
|
||||||
const ccconfig = getContext("cc-config");
|
|
||||||
const globalMetrics = getContext("globalMetrics");
|
|
||||||
const displayNodeOverview = (displayType === 'OVERVIEW')
|
|
||||||
|
|
||||||
const resampleConfig = getContext("resampling") || null;
|
|
||||||
const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
|
|
||||||
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
|
||||||
let selectedResolution = resampleConfig ? resampleDefault : 0;
|
|
||||||
|
|
||||||
let hostnameFilter = "";
|
|
||||||
let pendingHostnameFilter = "";
|
|
||||||
let selectedMetric = ccconfig.system_view_selectedMetric || "";
|
|
||||||
let selectedMetrics = (
|
|
||||||
ccconfig[`node_list_selectedMetrics:${cluster}:${subCluster}`] ||
|
|
||||||
ccconfig[`node_list_selectedMetrics:${cluster}`]
|
|
||||||
) || [ccconfig.system_view_selectedMetric];
|
|
||||||
let isMetricsSelectionOpen = false;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
|
Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
|
||||||
Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
|
Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let systemMetrics = [];
|
/* Scelte 5 Props */
|
||||||
let systemUnits = {};
|
let {
|
||||||
|
displayType,
|
||||||
|
cluster = null,
|
||||||
|
subCluster = null,
|
||||||
|
fromPreset = null,
|
||||||
|
toPreset = null,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
function loadMetrics(isInitialized) {
|
/* Const Init */
|
||||||
if (!isInitialized) return
|
const { query: initq } = init();
|
||||||
systemMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))]
|
const displayNodeOverview = (displayType === 'OVERVIEW');
|
||||||
for (let sm of systemMetrics) {
|
const ccconfig = getContext("cc-config");
|
||||||
systemUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
const initialized = getContext("initialized");
|
||||||
}
|
const globalMetrics = getContext("globalMetrics");
|
||||||
if (!selectedMetric) selectedMetric = systemMetrics[0].name
|
const resampleConfig = getContext("resampling") || null;
|
||||||
}
|
|
||||||
|
|
||||||
$: loadMetrics($initialized)
|
const resampleResolutions = resampleConfig ? [...resampleConfig.resolutions] : [];
|
||||||
|
const resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;
|
||||||
|
const nowDate = new Date(Date.now());
|
||||||
|
|
||||||
$: if (displayNodeOverview) {
|
/* State Init */
|
||||||
selectedMetrics = [selectedMetric]
|
let to = $state(toPreset || new Date(Date.now()));
|
||||||
}
|
let from = $state(fromPreset || new Date(nowDate.setHours(nowDate.getHours() - 4)));
|
||||||
|
let selectedResolution = $state(resampleConfig ? resampleDefault : 0);
|
||||||
|
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]);
|
||||||
|
|
||||||
$: { // Wait after input for some time to prevent too many requests
|
/* Derived States */
|
||||||
setTimeout(function () {
|
const systemMetrics = $derived($initialized ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
|
||||||
|
const presetSystemUnits = $derived(loadUnits(systemMetrics));
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
$effect(() => {
|
||||||
|
// OnMount: Ping Var, without this, OVERVIEW metric select is empty (reason tbd)
|
||||||
|
systemMetrics
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function loadUnits(systemMetrics) {
|
||||||
|
let pendingUnits = {};
|
||||||
|
if (systemMetrics.length > 0) {
|
||||||
|
for (let sm of systemMetrics) {
|
||||||
|
pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {...pendingUnits};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait after input for some time to prevent too many requests
|
||||||
|
let timeoutId = null;
|
||||||
|
function updateHostnameFilter() {
|
||||||
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(function () {
|
||||||
hostnameFilter = pendingHostnameFilter;
|
hostnameFilter = pendingHostnameFilter;
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- ROW1: Tools-->
|
<!-- ROW1: Tools-->
|
||||||
@@ -108,7 +110,7 @@
|
|||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => (isMetricsSelectionOpen = true)}
|
onclick={() => (isMetricsSelectionOpen = true)}
|
||||||
>
|
>
|
||||||
{selectedMetrics.length} selected
|
{selectedMetrics.length} selected
|
||||||
</Button>
|
</Button>
|
||||||
@@ -139,12 +141,20 @@
|
|||||||
placeholder="Filter hostname ..."
|
placeholder="Filter hostname ..."
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={pendingHostnameFilter}
|
bind:value={pendingHostnameFilter}
|
||||||
|
oninput={updateHostnameFilter}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Col>
|
</Col>
|
||||||
<!-- Range Col-->
|
<!-- Range Col-->
|
||||||
<Col>
|
<Col>
|
||||||
<TimeSelection bind:from bind:to />
|
<TimeSelection
|
||||||
|
presetFrom={from}
|
||||||
|
presetTo={to}
|
||||||
|
applyTime={(newFrom, newTo) => {
|
||||||
|
from = newFrom;
|
||||||
|
to = newTo;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<!-- Overview Metric Col-->
|
<!-- Overview Metric Col-->
|
||||||
{#if displayNodeOverview}
|
{#if displayNodeOverview}
|
||||||
@@ -153,26 +163,28 @@
|
|||||||
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
<InputGroupText><Icon name="graph-up" /></InputGroupText>
|
||||||
<InputGroupText>Metric</InputGroupText>
|
<InputGroupText>Metric</InputGroupText>
|
||||||
<Input type="select" bind:value={selectedMetric}>
|
<Input type="select" bind:value={selectedMetric}>
|
||||||
{#each systemMetrics as metric}
|
{#each systemMetrics as metric (metric.name)}
|
||||||
<option value={metric.name}
|
<option value={metric.name}
|
||||||
>{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
|
>{metric.name} {presetSystemUnits[metric.name] ? "("+presetSystemUnits[metric.name]+")" : ""}</option
|
||||||
>
|
>
|
||||||
|
{:else}
|
||||||
|
<option disabled>No available options</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Input>
|
</Input>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Col>
|
</Col>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- Refresh Col-->
|
|
||||||
<Col class="mt-2 mt-lg-0">
|
|
||||||
<Refresher
|
|
||||||
on:refresh={() => {
|
|
||||||
const diff = Date.now() - to;
|
|
||||||
from = new Date(from.getTime() + diff);
|
|
||||||
to = new Date(to.getTime() + diff);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<!-- Refresh Col-->
|
||||||
|
<Col class="mt-2 mt-lg-0">
|
||||||
|
<Refresher
|
||||||
|
onRefresh={() => {
|
||||||
|
const diff = Date.now() - to;
|
||||||
|
from = new Date(from.getTime() + diff);
|
||||||
|
to = new Date(to.getTime() + diff);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<!-- ROW2: Content-->
|
<!-- ROW2: Content-->
|
||||||
@@ -185,20 +197,22 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#if displayNodeOverview}
|
{#if displayNodeOverview}
|
||||||
<!-- ROW2-1: Node Overview (Grid Included)-->
|
<!-- ROW2-1: Node Overview (Grid Included)-->
|
||||||
<NodeOverview {cluster} {subCluster} {ccconfig} {selectedMetrics} {from} {to} {hostnameFilter}/>
|
<NodeOverview {cluster} {ccconfig} {selectedMetric} {from} {to} {hostnameFilter}/>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- ROW2-2: Node List (Grid Included)-->
|
<!-- ROW2-2: Node List (Grid Included)-->
|
||||||
<NodeList {cluster} {subCluster} {ccconfig} {selectedMetrics} {selectedResolution} {hostnameFilter} {from} {to} {systemUnits}/>
|
<NodeList {cluster} {subCluster} {ccconfig} {selectedMetrics} {selectedResolution} {hostnameFilter} {from} {to} {presetSystemUnits}/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<MetricSelection
|
{#if !displayNodeOverview}
|
||||||
{cluster}
|
<MetricSelection
|
||||||
{subCluster}
|
bind:isOpen={isMetricsSelectionOpen}
|
||||||
configName="node_list_selectedMetrics"
|
presetMetrics={selectedMetrics}
|
||||||
metrics={selectedMetrics}
|
{cluster}
|
||||||
bind:isOpen={isMetricsSelectionOpen}
|
{subCluster}
|
||||||
on:update-metrics={({ detail }) => {
|
configName="node_list_selectedMetrics"
|
||||||
selectedMetrics = [...detail]
|
applyMetrics={(newMetrics) =>
|
||||||
}}
|
selectedMetrics = [...newMetrics]
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -24,15 +24,22 @@
|
|||||||
init,
|
init,
|
||||||
} from "./generic/utils.js";
|
} from "./generic/utils.js";
|
||||||
|
|
||||||
export let username;
|
/* Svelte 5 Props */
|
||||||
export let isAdmin;
|
let {
|
||||||
export let tagmap;
|
username,
|
||||||
|
isAdmin,
|
||||||
|
presetTagmap,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const {} = init();
|
const {} = init();
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
|
|
||||||
let pendingChange = "none";
|
/* State Init */
|
||||||
|
let pendingChange = $state("none");
|
||||||
|
let tagmap = $state(presetTagmap)
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
const removeTagMutation = ({ tagIds }) => {
|
const removeTagMutation = ({ tagIds }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
@@ -96,7 +103,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => removeTag(tag, tagType)}
|
onclick={() => removeTag(tag, tagType)}
|
||||||
>
|
>
|
||||||
<Icon name="x" />
|
<Icon name="x" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -42,72 +42,102 @@
|
|||||||
import TextFilter from "./generic/helper/TextFilter.svelte"
|
import TextFilter from "./generic/helper/TextFilter.svelte"
|
||||||
import Refresher from "./generic/helper/Refresher.svelte";
|
import Refresher from "./generic/helper/Refresher.svelte";
|
||||||
|
|
||||||
|
/* Svelte 5 Props */
|
||||||
|
let { user, filterPresets } = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const { query: initq } = init();
|
const { query: initq } = init();
|
||||||
|
|
||||||
const ccconfig = getContext("cc-config");
|
const ccconfig = getContext("cc-config");
|
||||||
|
|
||||||
export let user;
|
|
||||||
export let filterPresets;
|
|
||||||
|
|
||||||
let filterComponent; // 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 jobList;
|
|
||||||
let jobFilters = [];
|
|
||||||
let matchedListJobs = 0;
|
|
||||||
let sorting = { field: "startTime", type: "col", order: "DESC" },
|
|
||||||
isSortingOpen = false;
|
|
||||||
let metrics = ccconfig.plot_list_selectedMetrics,
|
|
||||||
isMetricsSelectionOpen = false;
|
|
||||||
let isHistogramSelectionOpen = false;
|
|
||||||
let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null;
|
|
||||||
let showFootprint = filterPresets.cluster
|
|
||||||
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
|
||||||
: !!ccconfig.plot_list_showFootprint;
|
|
||||||
|
|
||||||
let numDurationBins = "1h";
|
|
||||||
let numMetricBins = 10;
|
|
||||||
let durationBinOptions = ["1m","10m","1h","6h","12h"];
|
|
||||||
let metricBinOptions = [10, 20, 50, 100];
|
|
||||||
|
|
||||||
$: selectedHistograms = selectedCluster
|
|
||||||
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
|
|
||||||
: ccconfig['user_view_histogramMetrics'] || [];
|
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
$: stats = queryStore({
|
const durationBinOptions = ["1m","10m","1h","6h","12h"];
|
||||||
client: client,
|
const metricBinOptions = [10, 20, 50, 100];
|
||||||
query: gql`
|
|
||||||
query ($jobFilters: [JobFilter!]!, $selectedHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) {
|
/* State Init */
|
||||||
jobsStatistics(filter: $jobFilters, metrics: $selectedHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) {
|
// List & Control Vars
|
||||||
totalJobs
|
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
|
||||||
shortJobs
|
let jobFilters = $state([]);
|
||||||
totalWalltime
|
let jobList = $state(null);
|
||||||
totalCoreHours
|
let matchedListJobs = $state(0);
|
||||||
histDuration {
|
let isSortingOpen = $state(false);
|
||||||
count
|
let isMetricsSelectionOpen = $state(false);
|
||||||
value
|
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
|
||||||
}
|
let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
|
||||||
histNumNodes {
|
let selectedHistogramsBuffer = $state({ all: (ccconfig['user_view_histogramMetrics'] || []) })
|
||||||
count
|
let metrics = $state(filterPresets.cluster
|
||||||
value
|
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
|
||||||
}
|
ccconfig.plot_list_selectedMetrics
|
||||||
histMetrics {
|
: ccconfig.plot_list_selectedMetrics
|
||||||
metric
|
);
|
||||||
unit
|
let showFootprint = $state(filterPresets.cluster
|
||||||
stat
|
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
|
||||||
data {
|
: !!ccconfig.plot_list_showFootprint
|
||||||
min
|
);
|
||||||
max
|
|
||||||
|
// 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 Vars */
|
||||||
|
let selectedHistograms = $derived(selectedCluster ? selectedHistogramsBuffer[selectedCluster] : selectedHistogramsBuffer['all']);
|
||||||
|
let stats = $derived(
|
||||||
|
queryStore({
|
||||||
|
client: client,
|
||||||
|
query: gql`
|
||||||
|
query ($jobFilters: [JobFilter!]!, $selectedHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) {
|
||||||
|
jobsStatistics(filter: $jobFilters, metrics: $selectedHistograms, numDurationBins: $numDurationBins , numMetricBins: $numMetricBins ) {
|
||||||
|
totalJobs
|
||||||
|
shortJobs
|
||||||
|
totalWalltime
|
||||||
|
totalCoreHours
|
||||||
|
histDuration {
|
||||||
count
|
count
|
||||||
bin
|
value
|
||||||
|
}
|
||||||
|
histNumNodes {
|
||||||
|
count
|
||||||
|
value
|
||||||
|
}
|
||||||
|
histMetrics {
|
||||||
|
metric
|
||||||
|
unit
|
||||||
|
stat
|
||||||
|
data {
|
||||||
|
min
|
||||||
|
max
|
||||||
|
count
|
||||||
|
bin
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
`,
|
||||||
`,
|
variables: { jobFilters, selectedHistograms, numDurationBins, numMetricBins },
|
||||||
variables: { jobFilters, selectedHistograms, numDurationBins, numMetricBins },
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Effect */
|
||||||
|
$effect(() => {
|
||||||
|
if (!selectedHistogramsBuffer[selectedCluster]) {
|
||||||
|
selectedHistogramsBuffer[selectedCluster] = ccconfig[`user_view_histogramMetrics:${selectedCluster}`];
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => filterComponent.updateFilters());
|
/* On Mount */
|
||||||
|
onMount(() => {
|
||||||
|
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}`];
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- ROW1: Status-->
|
<!-- ROW1: Status-->
|
||||||
@@ -129,13 +159,13 @@
|
|||||||
<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" on:click={() => (isSortingOpen = true)}>
|
<Button outline color="primary" onclick={() => (isSortingOpen = true)}>
|
||||||
<Icon name="sort-up" /> Sorting
|
<Icon name="sort-up" /> Sorting
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => (isMetricsSelectionOpen = true)}
|
onclick={() => (isMetricsSelectionOpen = true)}
|
||||||
>
|
>
|
||||||
<Icon name="graph-up" /> Metrics
|
<Icon name="graph-up" /> Metrics
|
||||||
</Button>
|
</Button>
|
||||||
@@ -143,11 +173,11 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col lg="4" class="mb-1 mb-lg-0">
|
<Col lg="4" class="mb-1 mb-lg-0">
|
||||||
<Filters
|
<Filters
|
||||||
|
bind:this={filterComponent}
|
||||||
{filterPresets}
|
{filterPresets}
|
||||||
matchedJobs={matchedListJobs}
|
matchedJobs={matchedListJobs}
|
||||||
startTimeQuickSelect={true}
|
startTimeQuickSelect={true}
|
||||||
bind:this={filterComponent}
|
applyFilters={(detail) => {
|
||||||
on:update-filters={({ 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
|
||||||
@@ -173,11 +203,11 @@
|
|||||||
</Col>
|
</Col>
|
||||||
<Col class="mb-2 mb-lg-0">
|
<Col class="mb-2 mb-lg-0">
|
||||||
<TextFilter
|
<TextFilter
|
||||||
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
|
setFilter={(filter) => filterComponent.updateFilters(filter)}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="mb-1 mb-lg-0">
|
<Col class="mb-1 mb-lg-0">
|
||||||
<Refresher on:refresh={() => {
|
<Refresher onRefresh={() => {
|
||||||
jobList.refreshJobs()
|
jobList.refreshJobs()
|
||||||
jobList.refreshAllMetrics()
|
jobList.refreshAllMetrics()
|
||||||
}} />
|
}} />
|
||||||
@@ -269,7 +299,7 @@
|
|||||||
outline
|
outline
|
||||||
color="secondary"
|
color="secondary"
|
||||||
class="w-100"
|
class="w-100"
|
||||||
on:click={() => (isHistogramSelectionOpen = true)}
|
onclick={() => (isHistogramSelectionOpen = true)}
|
||||||
>
|
>
|
||||||
<Icon name="bar-chart-line" /> Select Histograms
|
<Icon name="bar-chart-line" /> Select Histograms
|
||||||
</Button>
|
</Button>
|
||||||
@@ -344,19 +374,31 @@
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Sorting bind:sorting bind:isOpen={isSortingOpen} />
|
<Sorting
|
||||||
|
bind:isOpen={isSortingOpen}
|
||||||
|
presetSorting={sorting}
|
||||||
|
applySorting={(newSort) =>
|
||||||
|
sorting = {...newSort}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
bind:cluster={selectedCluster}
|
bind:isOpen={isMetricsSelectionOpen}
|
||||||
configName="plot_list_selectedMetrics"
|
bind:showFootprint
|
||||||
bind:metrics
|
presetMetrics={metrics}
|
||||||
bind:isOpen={isMetricsSelectionOpen}
|
cluster={selectedCluster}
|
||||||
bind:showFootprint
|
configName="plot_list_selectedMetrics"
|
||||||
footprintSelect
|
applyMetrics={(newMetrics) =>
|
||||||
|
metrics = [...newMetrics]
|
||||||
|
}
|
||||||
|
footprintSelect
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HistogramSelection
|
<HistogramSelection
|
||||||
bind:cluster={selectedCluster}
|
cluster={selectedCluster}
|
||||||
bind:selectedHistograms
|
|
||||||
bind:isOpen={isHistogramSelectionOpen}
|
bind:isOpen={isHistogramSelectionOpen}
|
||||||
|
presetSelectedHistograms={selectedHistograms}
|
||||||
|
applyChange={(newSelection) => {
|
||||||
|
selectedHistogramsBuffer[selectedCluster || 'all'] = [...newSelection];
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import Analysis from './Analysis.root.svelte'
|
import Analysis from './Analysis.root.svelte'
|
||||||
|
|
||||||
filterPresets.cluster = cluster
|
filterPresets.cluster = cluster
|
||||||
|
|
||||||
new Analysis({
|
mount(Analysis, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
filterPresets: filterPresets,
|
filterPresets: filterPresets,
|
||||||
|
|||||||
@@ -21,10 +21,14 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
|
|
||||||
export let availableMetrics;
|
/* Svelte 5 Props */
|
||||||
export let metricsInHistograms;
|
let {
|
||||||
export let metricsInScatterplots;
|
availableMetrics,
|
||||||
|
metricsInHistograms = $bindable(),
|
||||||
|
metricsInScatterplots = $bindable(),
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
@@ -38,11 +42,13 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let isHistogramConfigOpen = false,
|
/* State Init */
|
||||||
isScatterPlotConfigOpen = false;
|
let isHistogramConfigOpen = $state(false);
|
||||||
let selectedMetric1 = null,
|
let isScatterPlotConfigOpen = $state(false);
|
||||||
selectedMetric2 = null;
|
let selectedMetric1 = $state(null);
|
||||||
|
let selectedMetric2 = $state(null);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function updateConfiguration(data) {
|
function updateConfiguration(data) {
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -55,12 +61,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button outline on:click={() => (isHistogramConfigOpen = true)}>
|
<Button outline onclick={() => (isHistogramConfigOpen = true)}>
|
||||||
<Icon name="" />
|
<Icon name="" />
|
||||||
Select Plots for Histograms
|
Select Plots for Histograms
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button outline on:click={() => (isScatterPlotConfigOpen = true)}>
|
<Button outline onclick={() => (isScatterPlotConfigOpen = true)}>
|
||||||
<Icon name="" />
|
<Icon name="" />
|
||||||
Select Plots in Scatter Plots
|
Select Plots in Scatter Plots
|
||||||
</Button>
|
</Button>
|
||||||
@@ -78,7 +84,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:group={metricsInHistograms}
|
bind:group={metricsInHistograms}
|
||||||
value={metric}
|
value={metric}
|
||||||
on:change={() =>
|
onchange={() =>
|
||||||
updateConfiguration({
|
updateConfiguration({
|
||||||
name: "analysis_view_histogramMetrics",
|
name: "analysis_view_histogramMetrics",
|
||||||
value: metricsInHistograms,
|
value: metricsInHistograms,
|
||||||
@@ -91,7 +97,7 @@
|
|||||||
</ListGroup>
|
</ListGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={() => (isHistogramConfigOpen = false)}>
|
<Button color="primary" onclick={() => (isHistogramConfigOpen = false)}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@@ -112,7 +118,7 @@
|
|||||||
style="float: right;"
|
style="float: right;"
|
||||||
outline
|
outline
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
metricsInScatterplots = metricsInScatterplots.filter(
|
metricsInScatterplots = metricsInScatterplots.filter(
|
||||||
(p) => pair != p,
|
(p) => pair != p,
|
||||||
);
|
);
|
||||||
@@ -146,7 +152,7 @@
|
|||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
disabled={selectedMetric1 == null || selectedMetric2 == null}
|
disabled={selectedMetric1 == null || selectedMetric2 == null}
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
metricsInScatterplots = [
|
metricsInScatterplots = [
|
||||||
...metricsInScatterplots,
|
...metricsInScatterplots,
|
||||||
[selectedMetric1, selectedMetric2],
|
[selectedMetric1, selectedMetric2],
|
||||||
@@ -164,7 +170,7 @@
|
|||||||
</InputGroup>
|
</InputGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={() => (isScatterPlotConfigOpen = false)}>
|
<Button color="primary" onclick={() => (isScatterPlotConfigOpen = false)}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import Config from './Config.root.svelte'
|
import Config from './Config.root.svelte'
|
||||||
|
|
||||||
new Config({
|
mount(Config, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
|
|||||||
@@ -12,13 +12,17 @@
|
|||||||
import Options from "./admin/Options.svelte";
|
import Options from "./admin/Options.svelte";
|
||||||
import NoticeEdit from "./admin/NoticeEdit.svelte";
|
import NoticeEdit from "./admin/NoticeEdit.svelte";
|
||||||
|
|
||||||
export let ncontent;
|
/* Svelte 5 Props */
|
||||||
|
let { ncontent } = $props();
|
||||||
let users = [];
|
|
||||||
let roles = [];
|
|
||||||
|
|
||||||
|
/* Const Init*/
|
||||||
const ccconfig = getContext("cc-config");
|
const ccconfig = getContext("cc-config");
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let users = $state([]);
|
||||||
|
let roles = $state([]);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function getUserList() {
|
function getUserList() {
|
||||||
fetch("/config/users/?via-ldap=false¬-just-user=true")
|
fetch("/config/users/?via-ldap=false¬-just-user=true")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
@@ -40,21 +44,22 @@
|
|||||||
getValidRoles();
|
getValidRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* on Mount */
|
||||||
onMount(() => initAdmin());
|
onMount(() => initAdmin());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={2} class="p-2 g-2">
|
<Row cols={2} class="p-2 g-2">
|
||||||
<Col class="mb-1">
|
<Col class="mb-1">
|
||||||
<AddUser {roles} on:reload={getUserList} />
|
<AddUser {roles} reloadUser={() => getUserList()} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col class="mb-1">
|
<Col class="mb-1">
|
||||||
<ShowUsers on:reload={getUserList} bind:users />
|
<ShowUsers reloadUser={() => getUserList()} bind:users />
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<EditRole {roles} on:reload={getUserList} />
|
<EditRole {roles} reloadUser={() => getUserList()} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
<EditProject on:reload={getUserList} />
|
<EditProject reloadUser={() => getUserList()} />
|
||||||
</Col>
|
</Col>
|
||||||
<Options config={ccconfig}/>
|
<Options config={ccconfig}/>
|
||||||
<NoticeEdit {ncontent}/>
|
<NoticeEdit {ncontent}/>
|
||||||
|
|||||||
@@ -12,17 +12,26 @@
|
|||||||
import PlotRenderOptions from "./user/PlotRenderOptions.svelte";
|
import PlotRenderOptions from "./user/PlotRenderOptions.svelte";
|
||||||
import PlotColorScheme from "./user/PlotColorScheme.svelte";
|
import PlotColorScheme from "./user/PlotColorScheme.svelte";
|
||||||
|
|
||||||
export let username
|
/* Svelte 5 Props */
|
||||||
export let isApi
|
let {
|
||||||
|
username,
|
||||||
|
isApi
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const ccconfig = getContext("cc-config");
|
const ccconfig = getContext("cc-config");
|
||||||
let message = { msg: "", target: "", color: "#d63384" };
|
|
||||||
let displayMessage = false;
|
/* State Init */
|
||||||
let cbmode = ccconfig?.plot_general_colorblindMode || false;
|
let message = $state({ msg: "", target: "", color: "#d63384" });
|
||||||
|
let displayMessage = $state(false);
|
||||||
|
let cbmode = $state(ccconfig?.plot_general_colorblindMode || false);
|
||||||
|
|
||||||
async function handleSettingSubmit(event) {
|
/* Functions */
|
||||||
const selector = event.detail.selector
|
async function handleSettingSubmit(event, setting) {
|
||||||
const target = event.detail.target
|
event.preventDefault();
|
||||||
|
|
||||||
|
const selector = setting.selector
|
||||||
|
const target = setting.target
|
||||||
let form = document.querySelector(selector);
|
let form = document.querySelector(selector);
|
||||||
let formData = new FormData(form);
|
let formData = new FormData(form);
|
||||||
try {
|
try {
|
||||||
@@ -53,6 +62,6 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>
|
<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
|
||||||
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>
|
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
|
||||||
<PlotColorScheme config={ccconfig} bind:cbmode bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>
|
<PlotColorScheme config={ccconfig} bind:cbmode bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
|
||||||
|
|||||||
@@ -10,17 +10,19 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
|
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* Svelte 5 Props */
|
||||||
|
let { roles, reloadUser } = $props();
|
||||||
|
|
||||||
let message = { msg: "", color: "#d63384" };
|
/* State Init */
|
||||||
let displayMessage = false;
|
let message = $state({ msg: "", color: "#d63384" });
|
||||||
|
let displayMessage = $state(false);
|
||||||
|
|
||||||
export let roles;
|
/* Functions */
|
||||||
|
async function handleUserSubmit(event) {
|
||||||
async function handleUserSubmit() {
|
event.preventDefault();
|
||||||
|
|
||||||
let form = document.querySelector("#create-user-form");
|
let form = document.querySelector("#create-user-form");
|
||||||
let formData = new FormData(form);
|
let formData = new FormData(form);
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
popMessage(text, "#048109");
|
popMessage(text, "#048109");
|
||||||
reloadUserList();
|
reloadUser();
|
||||||
form.reset();
|
form.reset();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
@@ -47,10 +49,6 @@
|
|||||||
displayMessage = false;
|
displayMessage = false;
|
||||||
}, 3500);
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadUserList() {
|
|
||||||
dispatch("reload");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
@@ -59,7 +57,8 @@
|
|||||||
method="post"
|
method="post"
|
||||||
action="/config/users/"
|
action="/config/users/"
|
||||||
class="card-body"
|
class="card-body"
|
||||||
on:submit|preventDefault={handleUserSubmit}
|
autocomplete="off"
|
||||||
|
onsubmit={(e) => handleUserSubmit(e)}
|
||||||
>
|
>
|
||||||
<CardTitle class="mb-3">Create User</CardTitle>
|
<CardTitle class="mb-3">Create User</CardTitle>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -70,6 +69,7 @@
|
|||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
aria-describedby="usernameHelp"
|
aria-describedby="usernameHelp"
|
||||||
|
autocomplete="username"
|
||||||
/>
|
/>
|
||||||
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
<div id="usernameHelp" class="form-text">Must be unique.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,6 +81,7 @@
|
|||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
aria-describedby="passwordHelp"
|
aria-describedby="passwordHelp"
|
||||||
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
<div id="passwordHelp" class="form-text">
|
<div id="passwordHelp" class="form-text">
|
||||||
Only API users are allowed to have a blank password. Users with a blank
|
Only API users are allowed to have a blank password. Users with a blank
|
||||||
@@ -109,6 +110,7 @@
|
|||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
aria-describedby="nameHelp"
|
aria-describedby="nameHelp"
|
||||||
|
autocomplete="name"
|
||||||
/>
|
/>
|
||||||
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
|
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,6 +122,7 @@
|
|||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
aria-describedby="emailHelp"
|
aria-describedby="emailHelp"
|
||||||
|
autocomplete="email"
|
||||||
/>
|
/>
|
||||||
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
|
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,13 +156,13 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<p style="display: flex; align-items: center;">
|
<p style="display: flex; align-items: center;">
|
||||||
<Button type="submit" color="primary">Submit</Button>
|
<Button type="submit" color="primary" style="margin-right: 1.5em;">Submit</Button>
|
||||||
{#if displayMessage}<div style="margin-left: 1.5em;">
|
{#if displayMessage}
|
||||||
<b
|
<b
|
||||||
><code style="color: {message.color};" out:fade>{message.msg}</code
|
><code style="color: {message.color};" out:fade>{message.msg}</code
|
||||||
></b
|
></b
|
||||||
>
|
>
|
||||||
</div>{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -7,15 +7,19 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* Svelte 5 Props */
|
||||||
|
let { reloadUser } = $props();
|
||||||
|
|
||||||
let message = { msg: "", color: "#d63384" };
|
/* State Init */
|
||||||
let displayMessage = false;
|
let message = $state({ msg: "", color: "#d63384" });
|
||||||
|
let displayMessage = $state(false);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
async function handleAddProject(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
async function handleAddProject() {
|
|
||||||
const username = document.querySelector("#project-username").value;
|
const username = document.querySelector("#project-username").value;
|
||||||
const project = document.querySelector("#project-id").value;
|
const project = document.querySelector("#project-id").value;
|
||||||
|
|
||||||
@@ -36,7 +40,7 @@
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
popMessage(text, "#048109");
|
popMessage(text, "#048109");
|
||||||
reloadUserList();
|
reloadUser();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
throw new Error("Response Code " + res.status + "-> " + text);
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
@@ -46,7 +50,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveProject() {
|
async function handleRemoveProject(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const username = document.querySelector("#project-username").value;
|
const username = document.querySelector("#project-username").value;
|
||||||
const project = document.querySelector("#project-id").value;
|
const project = document.querySelector("#project-id").value;
|
||||||
|
|
||||||
@@ -67,7 +73,7 @@
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
popMessage(text, "#048109");
|
popMessage(text, "#048109");
|
||||||
reloadUserList();
|
reloadUser();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
throw new Error("Response Code " + res.status + "-> " + text);
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
@@ -84,10 +90,6 @@
|
|||||||
displayMessage = false;
|
displayMessage = false;
|
||||||
}, 3500);
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadUserList() {
|
|
||||||
dispatch("reload");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
@@ -108,19 +110,17 @@
|
|||||||
placeholder="project-id"
|
placeholder="project-id"
|
||||||
id="project-id"
|
id="project-id"
|
||||||
/>
|
/>
|
||||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
|
||||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
type="button"
|
type="button"
|
||||||
id="add-project-button"
|
id="add-project-button"
|
||||||
on:click|preventDefault={handleAddProject}>Add</button
|
onclick={(e) => handleAddProject(e)}>Add</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
type="button"
|
type="button"
|
||||||
id="remove-project-button"
|
id="remove-project-button"
|
||||||
on:click|preventDefault={handleRemoveProject}>Remove</button
|
onclick={(e) => handleRemoveProject(e)}>Remove</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -10,17 +10,19 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* SVelte 5 Props */
|
||||||
|
let {roles, reloadUser } = $props();
|
||||||
|
|
||||||
let message = { msg: "", color: "#d63384" };
|
/* State Init */
|
||||||
let displayMessage = false;
|
let message = $state({ msg: "", color: "#d63384" });
|
||||||
|
let displayMessage = $state(false);
|
||||||
|
|
||||||
export let roles;
|
/* Functions */
|
||||||
|
async function handleAddRole(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
async function handleAddRole() {
|
|
||||||
const username = document.querySelector("#role-username").value;
|
const username = document.querySelector("#role-username").value;
|
||||||
const role = document.querySelector("#role-select").value;
|
const role = document.querySelector("#role-select").value;
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
popMessage(text, "#048109");
|
popMessage(text, "#048109");
|
||||||
reloadUserList();
|
reloadUser();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
throw new Error("Response Code " + res.status + "-> " + text);
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
@@ -51,7 +53,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveRole() {
|
async function handleRemoveRole(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const username = document.querySelector("#role-username").value;
|
const username = document.querySelector("#role-username").value;
|
||||||
const role = document.querySelector("#role-select").value;
|
const role = document.querySelector("#role-select").value;
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
popMessage(text, "#048109");
|
popMessage(text, "#048109");
|
||||||
reloadUserList();
|
reloadUser();
|
||||||
} else {
|
} else {
|
||||||
let text = await res.text();
|
let text = await res.text();
|
||||||
throw new Error("Response Code " + res.status + "-> " + text);
|
throw new Error("Response Code " + res.status + "-> " + text);
|
||||||
@@ -89,10 +93,6 @@
|
|||||||
displayMessage = false;
|
displayMessage = false;
|
||||||
}, 3500);
|
}, 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadUserList() {
|
|
||||||
dispatch("reload");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
@@ -113,19 +113,17 @@
|
|||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
|
||||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
type="button"
|
type="button"
|
||||||
id="add-role-button"
|
id="add-role-button"
|
||||||
on:click|preventDefault={handleAddRole}>Add</button
|
onclick={(e) => handleAddRole(e)}>Add</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
type="button"
|
type="button"
|
||||||
id="remove-role-button"
|
id="remove-role-button"
|
||||||
on:click|preventDefault={handleRemoveRole}>Remove</button
|
onclick={(e) =>handleRemoveRole(e)}>Remove</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -6,14 +6,18 @@
|
|||||||
import { Col, Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
import { Col, Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
export let ncontent;
|
/* Svelte 5 Props */
|
||||||
|
let { ncontent } = $props();
|
||||||
|
|
||||||
let message = { msg: "", color: "#d63384" };
|
/* State Init */
|
||||||
let displayMessage = false;
|
let message = $state({ msg: "", color: "#d63384" });
|
||||||
|
let displayMessage = $state(false);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
async function handleEditNotice(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
async function handleEditNotice() {
|
|
||||||
const content = document.querySelector("#notice-content").value;
|
const content = document.querySelector("#notice-content").value;
|
||||||
|
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append("new-content", content);
|
formData.append("new-content", content);
|
||||||
|
|
||||||
@@ -56,14 +60,11 @@
|
|||||||
value={ncontent}
|
value={ncontent}
|
||||||
id="notice-content"
|
id="notice-content"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
|
|
||||||
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
type="button"
|
type="button"
|
||||||
id="edit-notice-button"
|
id="edit-notice-button"
|
||||||
on:click|preventDefault={handleEditNotice}>Edit Notice</button
|
onclick={(e) => handleEditNotice(e)}>Edit Notice</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
import { getContext, onMount } from "svelte";
|
import { getContext, onMount } from "svelte";
|
||||||
import { Col, Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
|
import { Col, Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
let scrambled;
|
/*Const Init */
|
||||||
|
|
||||||
const resampleConfig = getContext("resampling");
|
const resampleConfig = getContext("resampling");
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let scrambled = $state(false);
|
||||||
|
|
||||||
|
/* on Mount */
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
scrambled = window.localStorage.getItem("cc-scramble-names") != null;
|
scrambled = window.localStorage.getItem("cc-scramble-names") != null;
|
||||||
});
|
});
|
||||||
@@ -33,7 +36,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="scramble-names-checkbox"
|
id="scramble-names-checkbox"
|
||||||
style="margin-right: 1em;"
|
style="margin-right: 1em;"
|
||||||
on:click={handleScramble}
|
onclick={() => handleScramble()}
|
||||||
bind:checked={scrambled}
|
bind:checked={scrambled}
|
||||||
/>
|
/>
|
||||||
Active?
|
Active?
|
||||||
|
|||||||
@@ -16,23 +16,19 @@
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
CardBody,
|
CardBody,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import ShowUsersRow from "./ShowUsersRow.svelte";
|
import ShowUsersRow from "./ShowUsersRow.svelte";
|
||||||
|
|
||||||
export let users = [];
|
/*Svelte 5 Props */
|
||||||
|
let { users = $bindable([]), reloadUser } = $props();
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
function reloadUserList() {
|
|
||||||
dispatch("reload");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function deleteUser(username) {
|
function deleteUser(username) {
|
||||||
if (confirm("Are you sure?")) {
|
if (confirm("Are you sure?")) {
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append("username", username);
|
formData.append("username", username);
|
||||||
fetch("/config/users/", { method: "DELETE", body: formData }).then((res) => {
|
fetch("/config/users/", { method: "DELETE", body: formData }).then((res) => {
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
reloadUserList();
|
reloadUser();
|
||||||
} else {
|
} else {
|
||||||
confirm(res.statusText);
|
confirm(res.statusText);
|
||||||
}
|
}
|
||||||
@@ -40,7 +36,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: userList = users;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card class="h-100">
|
<Card class="h-100">
|
||||||
@@ -53,11 +48,11 @@
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
on:click={reloadUserList}
|
onclick={() => reloadUser()}
|
||||||
style="float: right;">Reload</Button
|
style="float: right;">Reload</Button
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<div style="width: 100%; max-height: 500px; overflow-y: scroll;">
|
<div style="width: 100%; max-height: 725px; overflow-y: scroll;">
|
||||||
<Table hover>
|
<Table hover>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -71,13 +66,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="users-list">
|
<tbody id="users-list">
|
||||||
{#each userList as user}
|
{#each users as user}
|
||||||
<tr id="user-{user.username}">
|
<tr id="user-{user.username}">
|
||||||
<ShowUsersRow {user} />
|
<ShowUsersRow {user} />
|
||||||
<td
|
<td
|
||||||
><button
|
><button
|
||||||
class="btn btn-danger del-user"
|
class="btn btn-danger del-user"
|
||||||
on:click={deleteUser(user.username)}>Delete</button
|
onclick={() => deleteUser(user.username)}>Delete</button
|
||||||
></td
|
></td
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -10,9 +10,13 @@
|
|||||||
import { Button } from "@sveltestrap/sveltestrap";
|
import { Button } from "@sveltestrap/sveltestrap";
|
||||||
import { fetchJwt } from "../../generic/utils.js"
|
import { fetchJwt } from "../../generic/utils.js"
|
||||||
|
|
||||||
export let user;
|
/* Svelte 5 Props */
|
||||||
|
let { user } = $props();
|
||||||
|
|
||||||
let jwt = "";
|
/* State Init */
|
||||||
|
let jwt = $state("");
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function getUserJwt(username) {
|
function getUserJwt(username) {
|
||||||
const p = fetchJwt(username);
|
const p = fetchJwt(username);
|
||||||
p.then((content) => {
|
p.then((content) => {
|
||||||
@@ -30,7 +34,7 @@
|
|||||||
<td><code>{user?.roles ? user.roles.join(", ") : "No Roles"}</code></td>
|
<td><code>{user?.roles ? user.roles.join(", ") : "No Roles"}</code></td>
|
||||||
<td>
|
<td>
|
||||||
{#if !jwt}
|
{#if !jwt}
|
||||||
<Button color="success" on:click={getUserJwt(user.username)}
|
<Button color="success" onclick={() => getUserJwt(user.username)}
|
||||||
>Gen. JWT</Button
|
>Gen. JWT</Button
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -3,15 +3,20 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Row, Col, Card, CardTitle, Button} from "@sveltestrap/sveltestrap";
|
import { Row, Col, Card, CardTitle, CardBody, Button} from "@sveltestrap/sveltestrap";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
export let config;
|
/* Svelte 5 Props */
|
||||||
|
let { config } = $props();
|
||||||
|
|
||||||
let message;
|
/* State Init */
|
||||||
let displayMessage;
|
let message = $state("");
|
||||||
|
let displayMessage = $state(false);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
async function handleSettingSubmit(event, selector, target) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
async function handleSettingSubmit(selector, target) {
|
|
||||||
let form = document.querySelector(selector);
|
let form = document.querySelector(selector);
|
||||||
let formData = new FormData(form);
|
let formData = new FormData(form);
|
||||||
try {
|
try {
|
||||||
@@ -42,48 +47,45 @@
|
|||||||
<Row cols={1} class="p-2 g-2">
|
<Row cols={1} class="p-2 g-2">
|
||||||
<Col>
|
<Col>
|
||||||
<Card class="h-100">
|
<Card class="h-100">
|
||||||
<form
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
id="node-paging-form"
|
<CardTitle class="m-3 d-flex align-items-center">
|
||||||
method="post"
|
<div>Node List Paging Type</div>
|
||||||
action="/frontend/configuration/"
|
{#if displayMessage && message.target == "npag"}<div
|
||||||
class="card-body"
|
style="margin-left: auto; font-size: 0.9em;"
|
||||||
on:submit|preventDefault={() =>
|
>
|
||||||
handleSettingSubmit("#node-paging-form", "npag")}
|
<code style="color: {message.color};" out:fade
|
||||||
>
|
>Update: {message.msg}</code
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
|
||||||
<CardTitle
|
|
||||||
style="margin-bottom: 1em; display: flex; align-items: center;"
|
|
||||||
>
|
|
||||||
<div>Node List Paging Type</div>
|
|
||||||
{#if displayMessage && message.target == "npag"}<div
|
|
||||||
style="margin-left: auto; font-size: 0.9em;"
|
|
||||||
>
|
>
|
||||||
<code style="color: {message.color};" out:fade
|
</div>{/if}
|
||||||
>Update: {message.msg}</code
|
</CardTitle>
|
||||||
>
|
<CardBody>
|
||||||
</div>{/if}
|
<form
|
||||||
</CardTitle>
|
id="node-paging-form"
|
||||||
<input type="hidden" name="key" value="node_list_usePaging" />
|
method="post"
|
||||||
<div class="mb-3">
|
action="/frontend/configuration/"
|
||||||
<div>
|
onsubmit={(e) => handleSettingSubmit(e, "#node-paging-form", "npag")}>
|
||||||
{#if config?.node_list_usePaging}
|
<input type="hidden" name="key" value="node_list_usePaging" />
|
||||||
<input type="radio" id="nodes-true-checked" name="value" value="true" checked />
|
<div class="mb-3">
|
||||||
{:else}
|
<div>
|
||||||
<input type="radio" id="nodes-true" name="value" value="true" />
|
{#if config?.node_list_usePaging}
|
||||||
{/if}
|
<input type="radio" id="nodes-true-checked" name="value" value="true" checked />
|
||||||
<label for="true">Paging with selectable count of nodes.</label>
|
{:else}
|
||||||
|
<input type="radio" id="nodes-true" name="value" value="true" />
|
||||||
|
{/if}
|
||||||
|
<label for="nodes-true">Paging with selectable count of nodes.</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{#if config?.node_list_usePaging}
|
||||||
|
<input type="radio" id="nodes-false" name="value" value="false" />
|
||||||
|
{:else}
|
||||||
|
<input type="radio" id="nodes-false-checked" name="value" value="false" checked />
|
||||||
|
{/if}
|
||||||
|
<label for="nodes-false">Continuous scroll iteratively adding 10 nodes.</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<Button color="primary" type="submit">Submit</Button>
|
||||||
{#if config?.node_list_usePaging}
|
</form>
|
||||||
<input type="radio" id="nodes-false" name="value" value="false" />
|
</CardBody>
|
||||||
{:else}
|
|
||||||
<input type="radio" id="nodes-false-checked" name="value" value="false" checked />
|
|
||||||
{/if}
|
|
||||||
<label for="false">Continuous scroll iteratively adding 10 nodes.</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button color="primary" type="submit">Submit</Button>
|
|
||||||
</form>
|
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -19,21 +19,20 @@
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
import { createEventDispatcher } from 'svelte';
|
|
||||||
|
|
||||||
export let config;
|
/* Svelte 5 Props */
|
||||||
export let message;
|
let {
|
||||||
export let displayMessage;
|
config,
|
||||||
export let cbmode = false;
|
message = $bindable(),
|
||||||
|
displayMessage = $bindable(),
|
||||||
|
cbmode = $bindable(false),
|
||||||
|
updateSetting
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* State Init */
|
||||||
function updateSetting(selector, target) {
|
let activeRow = $state(JSON.stringify(config?.plot_general_colorscheme));
|
||||||
dispatch('update-config', {
|
|
||||||
selector: selector,
|
/* Const Init */
|
||||||
target: target
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorschemes = {
|
const colorschemes = {
|
||||||
Default: [
|
Default: [
|
||||||
"#00bfff",
|
"#00bfff",
|
||||||
@@ -321,7 +320,6 @@
|
|||||||
"rgb(189,189,189)",
|
"rgb(189,189,189)",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={1} class="p-2 g-2">
|
<Row cols={1} class="p-2 g-2">
|
||||||
@@ -350,37 +348,34 @@
|
|||||||
<input type="hidden" name="key" value="plot_general_colorscheme" />
|
<input type="hidden" name="key" value="plot_general_colorscheme" />
|
||||||
<Table hover>
|
<Table hover>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each Object.entries(cbmode ? cvdschemes : colorschemes) as [name, rgbrow]}
|
{#key activeRow}
|
||||||
<tr>
|
{#each Object.entries(cbmode ? cvdschemes : colorschemes) as [name, rgbrow]}
|
||||||
<th scope="col">{name}</th>
|
<tr>
|
||||||
<td>
|
<th scope="col">{name}</th>
|
||||||
{#if rgbrow.join(",") == config.plot_general_colorscheme}
|
<td>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="value"
|
name="value"
|
||||||
value={JSON.stringify(rgbrow)}
|
value={JSON.stringify(rgbrow)}
|
||||||
checked
|
checked={activeRow == JSON.stringify(rgbrow)}
|
||||||
on:click={() =>
|
onclick={(e) => {
|
||||||
updateSetting("#colorscheme-form", "cs")}
|
activeRow = JSON.stringify(rgbrow)
|
||||||
|
updateSetting(e, {
|
||||||
|
selector: "#colorscheme-form",
|
||||||
|
target: "cs",
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{:else}
|
</td>
|
||||||
<input
|
<td>
|
||||||
type="radio"
|
{#each rgbrow as rgb}
|
||||||
name="value"
|
<span class="color-dot" style="background-color: {rgb};"
|
||||||
value={JSON.stringify(rgbrow)}
|
></span>
|
||||||
on:click={() =>
|
{/each}
|
||||||
updateSetting("#colorscheme-form", "cs")}
|
</td>
|
||||||
/>
|
</tr>
|
||||||
{/if}
|
{/each}
|
||||||
</td>
|
{/key}
|
||||||
<td>
|
|
||||||
{#each rgbrow as rgb}
|
|
||||||
<span class="color-dot" style="background-color: {rgb};"
|
|
||||||
></span>
|
|
||||||
{/each}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -19,33 +19,29 @@
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
import { createEventDispatcher } from 'svelte';
|
|
||||||
|
|
||||||
export let config;
|
/* Svelte 5 Props */
|
||||||
export let message;
|
let {
|
||||||
export let displayMessage;
|
config,
|
||||||
|
message = $bindable(),
|
||||||
const dispatch = createEventDispatcher();
|
displayMessage = $bindable(),
|
||||||
function updateSetting(selector, target) {
|
updateSetting
|
||||||
dispatch('update-config', {
|
} = $props();
|
||||||
selector: selector,
|
|
||||||
target: target
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={3} class="p-2 g-2">
|
<Row cols={3} class="p-2 g-2">
|
||||||
<!-- LINE WIDTH -->
|
<!-- LINE WIDTH -->
|
||||||
<Col
|
<Col
|
||||||
><Card class="h-100">
|
><Card class="h-100">
|
||||||
<!-- Important: Function with arguments needs to be event-triggered like on:submit={() => functionName('Some','Args')} OR no arguments and like this: on:submit={functionName} -->
|
|
||||||
<form
|
<form
|
||||||
id="line-width-form"
|
id="line-width-form"
|
||||||
method="post"
|
method="post"
|
||||||
action="/frontend/configuration/"
|
action="/frontend/configuration/"
|
||||||
class="card-body"
|
class="card-body"
|
||||||
on:submit|preventDefault={() =>
|
onsubmit={(e) => updateSetting(e, {
|
||||||
updateSetting("#line-width-form", "lw")}
|
selector: "#line-width-form",
|
||||||
|
target: "lw",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle
|
<CardTitle
|
||||||
@@ -90,8 +86,10 @@
|
|||||||
method="post"
|
method="post"
|
||||||
action="/frontend/configuration/"
|
action="/frontend/configuration/"
|
||||||
class="card-body"
|
class="card-body"
|
||||||
on:submit|preventDefault={() =>
|
onsubmit={(e) => updateSetting(e, {
|
||||||
updateSetting("#plots-per-row-form", "ppr")}
|
selector: "#plots-per-row-form",
|
||||||
|
target: "ppr",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle
|
<CardTitle
|
||||||
@@ -136,8 +134,10 @@
|
|||||||
method="post"
|
method="post"
|
||||||
action="/frontend/configuration/"
|
action="/frontend/configuration/"
|
||||||
class="card-body"
|
class="card-body"
|
||||||
on:submit|preventDefault={() =>
|
onsubmit={(e) => updateSetting(e, {
|
||||||
updateSetting("#backgrounds-form", "bg")}
|
selector: "#backgrounds-form",
|
||||||
|
target: "bg",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle
|
<CardTitle
|
||||||
@@ -156,17 +156,17 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div>
|
<div>
|
||||||
{#if config.plot_general_colorBackground}
|
{#if config.plot_general_colorBackground}
|
||||||
<input type="radio" id="true-checked" name="value" value="true" checked />
|
<input type="radio" id="colb-true-checked" name="value" value="true" checked />
|
||||||
{:else}
|
{:else}
|
||||||
<input type="radio" id="true" name="value" value="true" />
|
<input type="radio" id="colb-true" name="value" value="true" />
|
||||||
{/if}
|
{/if}
|
||||||
<label for="true">Yes</label>
|
<label for="true">Yes</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{#if config.plot_general_colorBackground}
|
{#if config.plot_general_colorBackground}
|
||||||
<input type="radio" id="false" name="value" value="false" />
|
<input type="radio" id="colb-false" name="value" value="false" />
|
||||||
{:else}
|
{:else}
|
||||||
<input type="radio" id="false-checked" name="value" value="false" checked />
|
<input type="radio" id="colb-false-checked" name="value" value="false" checked />
|
||||||
{/if}
|
{/if}
|
||||||
<label for="false">No</label>
|
<label for="false">No</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,8 +180,10 @@
|
|||||||
method="post"
|
method="post"
|
||||||
action="/frontend/configuration/"
|
action="/frontend/configuration/"
|
||||||
class="card-body"
|
class="card-body"
|
||||||
on:submit|preventDefault={() =>
|
onsubmit={(e) => updateSetting(e, {
|
||||||
updateSetting("#colorblindmode-form", "cbm")}
|
selector: "#colorblindmode-form",
|
||||||
|
target: "cbm",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle
|
<CardTitle
|
||||||
|
|||||||
@@ -22,16 +22,23 @@
|
|||||||
CardBody
|
CardBody
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
import { createEventDispatcher } from 'svelte';
|
|
||||||
import { fetchJwt } from "../../generic/utils.js";
|
import { fetchJwt } from "../../generic/utils.js";
|
||||||
|
|
||||||
export let config;
|
/* Svelte 5 Props */
|
||||||
export let message;
|
let {
|
||||||
export let displayMessage;
|
config,
|
||||||
export let username;
|
message = $bindable(),
|
||||||
export let isApi;
|
displayMessage = $bindable(),
|
||||||
|
username,
|
||||||
|
isApi,
|
||||||
|
updateSetting
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let jwt = "";
|
/* State Init */
|
||||||
|
let jwt = $state("");
|
||||||
|
let displayCheck = $state(false);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function getUserJwt(username) {
|
function getUserJwt(username) {
|
||||||
if (username) {
|
if (username) {
|
||||||
const p = fetchJwt(username);
|
const p = fetchJwt(username);
|
||||||
@@ -43,7 +50,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayCheck = false;
|
|
||||||
function clipJwt() {
|
function clipJwt() {
|
||||||
displayCheck = true;
|
displayCheck = true;
|
||||||
// Navigator clipboard api needs a secure context (https)
|
// Navigator clipboard api needs a secure context (https)
|
||||||
@@ -71,14 +77,6 @@
|
|||||||
displayCheck = false;
|
displayCheck = false;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
function updateSetting(selector, target) {
|
|
||||||
dispatch('update-config', {
|
|
||||||
selector: selector,
|
|
||||||
target: target
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={isApi ? 3 : 1} class="p-2 g-2">
|
<Row cols={isApi ? 3 : 1} class="p-2 g-2">
|
||||||
@@ -90,8 +88,10 @@
|
|||||||
method="post"
|
method="post"
|
||||||
action="/frontend/configuration/"
|
action="/frontend/configuration/"
|
||||||
class="card-body"
|
class="card-body"
|
||||||
on:submit|preventDefault={() =>
|
onsubmit={(e) => updateSetting(e, {
|
||||||
updateSetting("#paging-form", "pag")}
|
selector: "#paging-form",
|
||||||
|
target: "pag",
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->
|
||||||
<CardTitle
|
<CardTitle
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<input type="hidden" name="key" value="job_list_usePaging" />
|
<input type="hidden" name="key" value="job_list_usePaging" />
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div>
|
<div>
|
||||||
{#if config.job_list_usePaging}
|
{#if config?.job_list_usePaging}
|
||||||
<input type="radio" id="true-checked" name="value" value="true" checked />
|
<input type="radio" id="true-checked" name="value" value="true" checked />
|
||||||
{:else}
|
{:else}
|
||||||
<input type="radio" id="true" name="value" value="true" />
|
<input type="radio" id="true" name="value" value="true" />
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
<label for="true">Paging with selectable count of jobs.</label>
|
<label for="true">Paging with selectable count of jobs.</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{#if config.job_list_usePaging}
|
{#if config?.job_list_usePaging}
|
||||||
<input type="radio" id="false" name="value" value="false" />
|
<input type="radio" id="false" name="value" value="false" />
|
||||||
{:else}
|
{:else}
|
||||||
<input type="radio" id="false-checked" name="value" value="false" checked />
|
<input type="radio" id="false-checked" name="value" value="false" checked />
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<CardTitle>Generate JWT</CardTitle>
|
<CardTitle>Generate JWT</CardTitle>
|
||||||
{#if jwt}
|
{#if jwt}
|
||||||
<Button color="secondary" on:click={clipJwt()}>
|
<Button color="secondary" onclick={() => clipJwt()}>
|
||||||
Copy JWT to Clipboard
|
Copy JWT to Clipboard
|
||||||
</Button>
|
</Button>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<Button color="success" on:click={getUserJwt(username)}>
|
<Button color="success" onclick={() => getUserJwt(username)}>
|
||||||
Generate JWT for '{username}'
|
Generate JWT for '{username}'
|
||||||
</Button>
|
</Button>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import {
|
import {
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -26,78 +25,77 @@
|
|||||||
ButtonDropdown,
|
ButtonDropdown,
|
||||||
Icon,
|
Icon,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import Tag from "./helper/Tag.svelte";
|
|
||||||
import Info from "./filters/InfoBox.svelte";
|
import Info from "./filters/InfoBox.svelte";
|
||||||
import Cluster from "./filters/Cluster.svelte";
|
import Cluster from "./filters/Cluster.svelte";
|
||||||
import JobStates, { allJobStates } from "./filters/JobStates.svelte";
|
import JobStates, { allJobStates } from "./filters/JobStates.svelte";
|
||||||
import StartTime from "./filters/StartTime.svelte";
|
import StartTime, { startTimeSelectOptions } from "./filters/StartTime.svelte";
|
||||||
import Tags from "./filters/Tags.svelte";
|
|
||||||
import Duration from "./filters/Duration.svelte";
|
import Duration from "./filters/Duration.svelte";
|
||||||
import Energy from "./filters/Energy.svelte";
|
import Tags from "./filters/Tags.svelte";
|
||||||
|
import Tag from "./helper/Tag.svelte";
|
||||||
import Resources from "./filters/Resources.svelte";
|
import Resources from "./filters/Resources.svelte";
|
||||||
|
import Energy from "./filters/Energy.svelte";
|
||||||
import Statistics from "./filters/Stats.svelte";
|
import Statistics from "./filters/Stats.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
export let menuText = null;
|
menuText = null,
|
||||||
export let filterPresets = {};
|
filterPresets = {},
|
||||||
export let disableClusterSelection = false;
|
disableClusterSelection = false,
|
||||||
export let startTimeQuickSelect = false;
|
startTimeQuickSelect = false,
|
||||||
export let matchedJobs = -2;
|
matchedJobs = -2,
|
||||||
export let showFilter = true;
|
showFilter = true,
|
||||||
|
applyFilters
|
||||||
const startTimeSelectOptions = [
|
} = $props();
|
||||||
{ range: "", rangeLabel: "No Selection"},
|
|
||||||
{ range: "last6h", rangeLabel: "Last 6hrs"},
|
|
||||||
{ range: "last24h", rangeLabel: "Last 24hrs"},
|
|
||||||
{ range: "last7d", rangeLabel: "Last 7 days"},
|
|
||||||
{ range: "last30d", rangeLabel: "Last 30 days"}
|
|
||||||
];
|
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const nodeMatchLabels = {
|
const nodeMatchLabels = {
|
||||||
eq: "",
|
eq: "",
|
||||||
contains: " Contains",
|
contains: " Contains",
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterReset = {
|
const filterReset = {
|
||||||
projectMatch: "contains",
|
// Direct Filters
|
||||||
userMatch: "contains",
|
dbId: [],
|
||||||
|
jobId: "",
|
||||||
jobIdMatch: "eq",
|
jobIdMatch: "eq",
|
||||||
nodeMatch: "eq",
|
arrayJobId: null,
|
||||||
|
jobName: "",
|
||||||
|
// View Filters
|
||||||
|
project: "",
|
||||||
|
projectMatch: "contains",
|
||||||
|
user: "",
|
||||||
|
userMatch: "contains",
|
||||||
|
// Filter Modals
|
||||||
cluster: null,
|
cluster: null,
|
||||||
partition: null,
|
partition: null,
|
||||||
states: allJobStates,
|
states: allJobStates,
|
||||||
startTime: { from: null, to: null, range: ""},
|
startTime: { from: null, to: null, range: ""},
|
||||||
tags: [],
|
|
||||||
duration: {
|
duration: {
|
||||||
lessThan: null,
|
lessThan: null,
|
||||||
moreThan: null,
|
moreThan: null,
|
||||||
from: null,
|
from: null,
|
||||||
to: null,
|
to: null,
|
||||||
},
|
},
|
||||||
dbId: [],
|
tags: [],
|
||||||
jobId: "",
|
|
||||||
arrayJobId: null,
|
|
||||||
user: "",
|
|
||||||
project: "",
|
|
||||||
jobName: "",
|
|
||||||
|
|
||||||
node: null,
|
|
||||||
energy: { from: null, to: null },
|
|
||||||
numNodes: { from: null, to: null },
|
numNodes: { from: null, to: null },
|
||||||
numHWThreads: { from: null, to: null },
|
numHWThreads: { from: null, to: null },
|
||||||
numAccelerators: { from: null, to: null },
|
numAccelerators: { from: null, to: null },
|
||||||
|
node: null,
|
||||||
|
nodeMatch: "eq",
|
||||||
|
energy: { from: null, to: null },
|
||||||
stats: [],
|
stats: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
let filters = {
|
/* State Init */
|
||||||
projectMatch: filterPresets.projectMatch || "contains",
|
let filters = $state({
|
||||||
userMatch: filterPresets.userMatch || "contains",
|
dbId: filterPresets.dbId || [],
|
||||||
|
jobId: filterPresets.jobId || "",
|
||||||
jobIdMatch: filterPresets.jobIdMatch || "eq",
|
jobIdMatch: filterPresets.jobIdMatch || "eq",
|
||||||
nodeMatch: filterPresets.nodeMatch || "eq",
|
arrayJobId: filterPresets.arrayJobId || null,
|
||||||
|
jobName: filterPresets.jobName || "",
|
||||||
|
project: filterPresets.project || "",
|
||||||
|
projectMatch: filterPresets.projectMatch || "contains",
|
||||||
|
user: filterPresets.user || "",
|
||||||
|
userMatch: filterPresets.userMatch || "contains",
|
||||||
cluster: filterPresets.cluster || null,
|
cluster: filterPresets.cluster || null,
|
||||||
partition: filterPresets.partition || null,
|
partition: filterPresets.partition || null,
|
||||||
states:
|
states:
|
||||||
@@ -105,41 +103,33 @@
|
|||||||
? [filterPresets.state].flat()
|
? [filterPresets.state].flat()
|
||||||
: allJobStates,
|
: allJobStates,
|
||||||
startTime: filterPresets.startTime || { from: null, to: null, range: ""},
|
startTime: filterPresets.startTime || { from: null, to: null, range: ""},
|
||||||
tags: filterPresets.tags || [],
|
|
||||||
duration: filterPresets.duration || {
|
duration: filterPresets.duration || {
|
||||||
lessThan: null,
|
lessThan: null,
|
||||||
moreThan: null,
|
moreThan: null,
|
||||||
from: null,
|
from: null,
|
||||||
to: null,
|
to: null,
|
||||||
},
|
},
|
||||||
dbId: filterPresets.dbId || [],
|
tags: filterPresets.tags || [],
|
||||||
jobId: filterPresets.jobId || "",
|
|
||||||
arrayJobId: filterPresets.arrayJobId || null,
|
|
||||||
user: filterPresets.user || "",
|
|
||||||
project: filterPresets.project || "",
|
|
||||||
jobName: filterPresets.jobName || "",
|
|
||||||
|
|
||||||
node: filterPresets.node || null,
|
|
||||||
energy: filterPresets.energy || { from: null, to: null },
|
|
||||||
numNodes: filterPresets.numNodes || { from: null, to: null },
|
numNodes: filterPresets.numNodes || { from: null, to: null },
|
||||||
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
|
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
|
||||||
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
|
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
|
||||||
|
node: filterPresets.node || null,
|
||||||
|
nodeMatch: filterPresets.nodeMatch || "eq",
|
||||||
|
energy: filterPresets.energy || { from: null, to: null },
|
||||||
stats: filterPresets.stats || [],
|
stats: filterPresets.stats || [],
|
||||||
};
|
});
|
||||||
|
|
||||||
let isClusterOpen = false,
|
/* Opened States */
|
||||||
isJobStatesOpen = false,
|
let isClusterOpen = $state(false)
|
||||||
isStartTimeOpen = false,
|
let isJobStatesOpen = $state(false)
|
||||||
isTagsOpen = false,
|
let isStartTimeOpen = $state(false)
|
||||||
isDurationOpen = false,
|
let isDurationOpen = $state(false)
|
||||||
isEnergyOpen = false,
|
let isTagsOpen = $state(false)
|
||||||
isResourcesOpen = false,
|
let isResourcesOpen = $state(false)
|
||||||
isStatsOpen = false,
|
let isEnergyOpen = $state(false)
|
||||||
isNodesModified = false,
|
let isStatsOpen = $state(false)
|
||||||
isHwthreadsModified = false,
|
|
||||||
isAccsModified = false;
|
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
// Can be called from the outside to trigger a 'update' event from this component.
|
// Can be called from the outside to trigger a 'update' event from this component.
|
||||||
// 'force' option empties existing filters and then applies only 'additionalFilters'
|
// 'force' option empties existing filters and then applies only 'additionalFilters'
|
||||||
export function updateFilters(additionalFilters = null, force = false) {
|
export function updateFilters(additionalFilters = null, force = false) {
|
||||||
@@ -153,10 +143,20 @@
|
|||||||
}
|
}
|
||||||
// Construct New Filter
|
// Construct New Filter
|
||||||
let items = [];
|
let items = [];
|
||||||
|
if (filters.dbId.length != 0)
|
||||||
|
items.push({ dbId: filters.dbId });
|
||||||
|
if (filters.jobId)
|
||||||
|
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
|
||||||
|
if (filters.arrayJobId != null)
|
||||||
|
items.push({ arrayJobId: filters.arrayJobId });
|
||||||
|
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
|
||||||
|
if (filters.project)
|
||||||
|
items.push({ project: { [filters.projectMatch]: filters.project } });
|
||||||
|
if (filters.user)
|
||||||
|
items.push({ user: { [filters.userMatch]: filters.user } });
|
||||||
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
|
if (filters.cluster) items.push({ cluster: { eq: filters.cluster } });
|
||||||
if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
|
|
||||||
if (filters.partition) items.push({ partition: { eq: filters.partition } });
|
if (filters.partition) items.push({ partition: { eq: filters.partition } });
|
||||||
if (filters.states.length != allJobStates.length)
|
if (filters.states.length != allJobStates?.length)
|
||||||
items.push({ state: filters.states });
|
items.push({ state: filters.states });
|
||||||
if (filters.startTime.from || filters.startTime.to)
|
if (filters.startTime.from || filters.startTime.to)
|
||||||
items.push({
|
items.push({
|
||||||
@@ -166,7 +166,6 @@
|
|||||||
items.push({
|
items.push({
|
||||||
startTime: { range: filters.startTime.range },
|
startTime: { range: filters.startTime.range },
|
||||||
});
|
});
|
||||||
if (filters.tags.length != 0) items.push({ tags: filters.tags });
|
|
||||||
if (filters.duration.from || filters.duration.to)
|
if (filters.duration.from || filters.duration.to)
|
||||||
items.push({
|
items.push({
|
||||||
duration: { from: filters.duration.from, to: filters.duration.to },
|
duration: { from: filters.duration.from, to: filters.duration.to },
|
||||||
@@ -175,21 +174,11 @@
|
|||||||
items.push({ duration: { from: 0, to: filters.duration.lessThan } });
|
items.push({ duration: { from: 0, to: filters.duration.lessThan } });
|
||||||
if (filters.duration.moreThan)
|
if (filters.duration.moreThan)
|
||||||
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes
|
items.push({ duration: { from: filters.duration.moreThan, to: 604800 } }); // 7 days to include special jobs with long runtimes
|
||||||
if (filters.energy.from || filters.energy.to)
|
if (filters.tags.length != 0) items.push({ tags: filters.tags });
|
||||||
items.push({
|
|
||||||
energy: { from: filters.energy.from, to: filters.energy.to },
|
|
||||||
});
|
|
||||||
if (filters.dbId.length != 0)
|
|
||||||
items.push({ dbId: filters.dbId });
|
|
||||||
if (filters.jobId)
|
|
||||||
items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } });
|
|
||||||
if (filters.arrayJobId != null)
|
|
||||||
items.push({ arrayJobId: filters.arrayJobId });
|
|
||||||
if (filters.numNodes.from != null || filters.numNodes.to != null) {
|
if (filters.numNodes.from != null || filters.numNodes.to != null) {
|
||||||
items.push({
|
items.push({
|
||||||
numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
|
numNodes: { from: filters.numNodes.from, to: filters.numNodes.to },
|
||||||
});
|
});
|
||||||
isNodesModified = true;
|
|
||||||
}
|
}
|
||||||
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) {
|
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -198,7 +187,6 @@
|
|||||||
to: filters.numHWThreads.to,
|
to: filters.numHWThreads.to,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
isHwthreadsModified = true;
|
|
||||||
}
|
}
|
||||||
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) {
|
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -207,17 +195,16 @@
|
|||||||
to: filters.numAccelerators.to,
|
to: filters.numAccelerators.to,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
isAccsModified = true;
|
|
||||||
}
|
}
|
||||||
if (filters.user)
|
if (filters.node) items.push({ node: { [filters.nodeMatch]: filters.node } });
|
||||||
items.push({ user: { [filters.userMatch]: filters.user } });
|
if (filters.energy.from || filters.energy.to)
|
||||||
if (filters.project)
|
items.push({
|
||||||
items.push({ project: { [filters.projectMatch]: filters.project } });
|
energy: { from: filters.energy.from, to: filters.energy.to },
|
||||||
if (filters.jobName) items.push({ jobName: { contains: filters.jobName } });
|
});
|
||||||
if (filters.stats.length != 0)
|
if (filters.stats.length != 0)
|
||||||
items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) });
|
items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) });
|
||||||
|
|
||||||
dispatch("update-filters", { filters: items });
|
applyFilters({ filters: items });
|
||||||
changeURL();
|
changeURL();
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -226,20 +213,7 @@
|
|||||||
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000);
|
||||||
let opts = [];
|
let opts = [];
|
||||||
|
|
||||||
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
|
// Direct Filters
|
||||||
if (filters.node) opts.push(`node=${filters.node}`);
|
|
||||||
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
|
|
||||||
opts.push(`nodeMatch=${filters.nodeMatch}`);
|
|
||||||
if (filters.partition) opts.push(`partition=${filters.partition}`);
|
|
||||||
if (filters.states.length != allJobStates.length)
|
|
||||||
for (let state of filters.states) opts.push(`state=${state}`);
|
|
||||||
if (filters.startTime.from && filters.startTime.to)
|
|
||||||
opts.push(
|
|
||||||
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
|
|
||||||
);
|
|
||||||
if (filters.startTime.range) {
|
|
||||||
opts.push(`startTime=${filters.startTime.range}`)
|
|
||||||
}
|
|
||||||
if (filters.dbId.length != 0) {
|
if (filters.dbId.length != 0) {
|
||||||
for (let dbi of filters.dbId) {
|
for (let dbi of filters.dbId) {
|
||||||
opts.push(`dbId=${dbi}`);
|
opts.push(`dbId=${dbi}`);
|
||||||
@@ -254,21 +228,12 @@
|
|||||||
}
|
}
|
||||||
if (filters.jobIdMatch != "eq")
|
if (filters.jobIdMatch != "eq")
|
||||||
opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case
|
opts.push(`jobIdMatch=${filters.jobIdMatch}`); // "eq" is default-case
|
||||||
for (let tag of filters.tags) opts.push(`tag=${tag}`);
|
if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
|
||||||
if (filters.duration.from && filters.duration.to)
|
if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
|
||||||
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
|
// View Filters
|
||||||
if (filters.duration.lessThan)
|
if (filters.project) opts.push(`project=${filters.project}`);
|
||||||
opts.push(`duration=0-${filters.duration.lessThan}`);
|
if (filters.project && filters.projectMatch != "contains") // "contains" is default-case
|
||||||
if (filters.duration.moreThan)
|
opts.push(`projectMatch=${filters.projectMatch}`);
|
||||||
opts.push(`duration=${filters.duration.moreThan}-604800`);
|
|
||||||
if (filters.energy.from && filters.energy.to)
|
|
||||||
opts.push(`energy=${filters.energy.from}-${filters.energy.to}`);
|
|
||||||
if (filters.numNodes.from && filters.numNodes.to)
|
|
||||||
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
|
|
||||||
if (filters.numHWThreads.from && filters.numHWThreads.to)
|
|
||||||
opts.push(`numHWThreads=${filters.numHWThreads.from}-${filters.numHWThreads.to}`);
|
|
||||||
if (filters.numAccelerators.from && filters.numAccelerators.to)
|
|
||||||
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`);
|
|
||||||
if (filters.user.length != 0)
|
if (filters.user.length != 0)
|
||||||
if (filters.userMatch != "in") {
|
if (filters.userMatch != "in") {
|
||||||
opts.push(`user=${filters.user}`);
|
opts.push(`user=${filters.user}`);
|
||||||
@@ -277,16 +242,42 @@
|
|||||||
}
|
}
|
||||||
if (filters.userMatch != "contains") // "contains" is default-case
|
if (filters.userMatch != "contains") // "contains" is default-case
|
||||||
opts.push(`userMatch=${filters.userMatch}`);
|
opts.push(`userMatch=${filters.userMatch}`);
|
||||||
if (filters.project) opts.push(`project=${filters.project}`);
|
// Filter Modals
|
||||||
if (filters.project && filters.projectMatch != "contains") // "contains" is default-case
|
if (filters.cluster) opts.push(`cluster=${filters.cluster}`);
|
||||||
opts.push(`projectMatch=${filters.projectMatch}`);
|
if (filters.partition) opts.push(`partition=${filters.partition}`);
|
||||||
if (filters.jobName) opts.push(`jobName=${filters.jobName}`);
|
if (filters.states.length != allJobStates?.length)
|
||||||
if (filters.arrayJobId) opts.push(`arrayJobId=${filters.arrayJobId}`);
|
for (let state of filters.states) opts.push(`state=${state}`);
|
||||||
|
if (filters.startTime.from && filters.startTime.to)
|
||||||
|
opts.push(
|
||||||
|
`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`,
|
||||||
|
);
|
||||||
|
if (filters.startTime.range) {
|
||||||
|
opts.push(`startTime=${filters.startTime.range}`)
|
||||||
|
}
|
||||||
|
if (filters.duration.from && filters.duration.to)
|
||||||
|
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`);
|
||||||
|
if (filters.duration.lessThan)
|
||||||
|
opts.push(`duration=0-${filters.duration.lessThan}`);
|
||||||
|
if (filters.duration.moreThan)
|
||||||
|
opts.push(`duration=${filters.duration.moreThan}-604800`);
|
||||||
|
if (filters.tags.length != 0)
|
||||||
|
for (let tag of filters.tags) opts.push(`tag=${tag}`);
|
||||||
|
if (filters.numNodes.from && filters.numNodes.to)
|
||||||
|
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`);
|
||||||
|
if (filters.numHWThreads.from && filters.numHWThreads.to)
|
||||||
|
opts.push(`numHWThreads=${filters.numHWThreads.from}-${filters.numHWThreads.to}`);
|
||||||
|
if (filters.numAccelerators.from && filters.numAccelerators.to)
|
||||||
|
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`);
|
||||||
|
if (filters.node) opts.push(`node=${filters.node}`);
|
||||||
|
if (filters.node && filters.nodeMatch != "eq") // "eq" is default-case
|
||||||
|
opts.push(`nodeMatch=${filters.nodeMatch}`);
|
||||||
|
if (filters.energy.from && filters.energy.to)
|
||||||
|
opts.push(`energy=${filters.energy.from}-${filters.energy.to}`);
|
||||||
if (filters.stats.length != 0)
|
if (filters.stats.length != 0)
|
||||||
for (let stat of filters.stats) {
|
for (let stat of filters.stats) {
|
||||||
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
|
opts.push(`stat=${stat.field}-${stat.from}-${stat.to}`);
|
||||||
}
|
}
|
||||||
|
// Build && Return
|
||||||
if (opts.length == 0 && window.location.search.length <= 1) return;
|
if (opts.length == 0 && window.location.search.length <= 1) return;
|
||||||
let newurl = `${window.location.pathname}?${opts.join("&")}`;
|
let newurl = `${window.location.pathname}?${opts.join("&")}`;
|
||||||
window.history.replaceState(null, "", newurl);
|
window.history.replaceState(null, "", newurl);
|
||||||
@@ -296,7 +287,7 @@
|
|||||||
<!-- Dropdown-Button -->
|
<!-- Dropdown-Button -->
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{#if showFilter}
|
{#if showFilter}
|
||||||
<ButtonDropdown class="cc-dropdown-on-hover mb-1" style="{(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}">
|
<ButtonDropdown class="cc-dropdown-on-hover mb-1" style={(matchedJobs >= -1) ? '' : 'margin-right: 0.5rem;'}>
|
||||||
<DropdownToggle outline caret color="success">
|
<DropdownToggle outline caret color="success">
|
||||||
<Icon name="sliders" />
|
<Icon name="sliders" />
|
||||||
Filters
|
Filters
|
||||||
@@ -307,36 +298,36 @@
|
|||||||
<DropdownItem disabled>{menuText}</DropdownItem>
|
<DropdownItem disabled>{menuText}</DropdownItem>
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
{/if}
|
{/if}
|
||||||
<DropdownItem on:click={() => (isClusterOpen = true)}>
|
<DropdownItem onclick={() => (isClusterOpen = true)}>
|
||||||
<Icon name="cpu" /> Cluster/Partition
|
<Icon name="cpu" /> Cluster/Partition
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
|
<DropdownItem onclick={() => (isJobStatesOpen = true)}>
|
||||||
<Icon name="gear-fill" /> Job States
|
<Icon name="gear-fill" /> Job States
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
|
<DropdownItem onclick={() => (isStartTimeOpen = true)}>
|
||||||
<Icon name="calendar-range" /> Start Time
|
<Icon name="calendar-range" /> Start Time
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isDurationOpen = true)}>
|
<DropdownItem onclick={() => (isDurationOpen = true)}>
|
||||||
<Icon name="stopwatch" /> Duration
|
<Icon name="stopwatch" /> Duration
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isTagsOpen = true)}>
|
<DropdownItem onclick={() => (isTagsOpen = true)}>
|
||||||
<Icon name="tags" /> Tags
|
<Icon name="tags" /> Tags
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isResourcesOpen = true)}>
|
<DropdownItem onclick={() => (isResourcesOpen = true)}>
|
||||||
<Icon name="hdd-stack" /> Resources
|
<Icon name="hdd-stack" /> Resources
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isEnergyOpen = true)}>
|
<DropdownItem onclick={() => (isEnergyOpen = true)}>
|
||||||
<Icon name="lightning-charge-fill" /> Energy
|
<Icon name="lightning-charge-fill" /> Energy
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem on:click={() => (isStatsOpen = true)}>
|
<DropdownItem onclick={() => (isStatsOpen = true)}>
|
||||||
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)} /> Statistics
|
<Icon name="bar-chart" onclick={() => (isStatsOpen = true)} /> Statistics
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{#if startTimeQuickSelect}
|
{#if startTimeQuickSelect}
|
||||||
<DropdownItem divider />
|
<DropdownItem divider />
|
||||||
<DropdownItem disabled>Start Time Quick Selection</DropdownItem>
|
<DropdownItem disabled>Start Time Quick Selection</DropdownItem>
|
||||||
{#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
|
{#each startTimeSelectOptions.filter((stso) => stso.range !== "") as { rangeLabel, range }}
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
filters.startTime.from = null
|
filters.startTime.from = null
|
||||||
filters.startTime.to = null
|
filters.startTime.to = null
|
||||||
filters.startTime.range = range;
|
filters.startTime.range = range;
|
||||||
@@ -353,7 +344,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if matchedJobs >= -1}
|
{#if matchedJobs >= -1}
|
||||||
<Button class="mb-1" style="margin-right: 0.5rem;" disabled outline>
|
<Button class="mb-1" style="margin-right: 0.25rem;" disabled outline>
|
||||||
{matchedJobs == -1 ? 'Loading ...' : `${matchedJobs} jobs`}
|
{matchedJobs == -1 ? 'Loading ...' : `${matchedJobs} jobs`}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -362,7 +353,7 @@
|
|||||||
{#if showFilter}
|
{#if showFilter}
|
||||||
<!-- SELECTED FILTER PILLS -->
|
<!-- SELECTED FILTER PILLS -->
|
||||||
{#if filters.cluster}
|
{#if filters.cluster}
|
||||||
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
|
<Info icon="cpu" onclick={() => (isClusterOpen = true)}>
|
||||||
{filters.cluster}
|
{filters.cluster}
|
||||||
{#if filters.partition}
|
{#if filters.partition}
|
||||||
({filters.partition})
|
({filters.partition})
|
||||||
@@ -370,14 +361,14 @@
|
|||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.states.length != allJobStates.length}
|
{#if filters.states.length != allJobStates?.length}
|
||||||
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
|
<Info icon="gear-fill" onclick={() => (isJobStatesOpen = true)}>
|
||||||
{filters.states.join(", ")}
|
{filters.states.join(", ")}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.startTime.from || filters.startTime.to}
|
{#if filters.startTime.from || filters.startTime.to}
|
||||||
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
|
<Info icon="calendar-range" onclick={() => (isStartTimeOpen = true)}>
|
||||||
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
|
{new Date(filters.startTime.from).toLocaleString()} - {new Date(
|
||||||
filters.startTime.to,
|
filters.startTime.to,
|
||||||
).toLocaleString()}
|
).toLocaleString()}
|
||||||
@@ -385,13 +376,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.startTime.range}
|
{#if filters.startTime.range}
|
||||||
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
|
<Info icon="calendar-range" onclick={() => (isStartTimeOpen = true)}>
|
||||||
{startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel }
|
{startTimeSelectOptions.find((stso) => stso.range === filters.startTime.range).rangeLabel }
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.from || filters.duration.to}
|
{#if filters.duration.from || filters.duration.to}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
|
||||||
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
|
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(
|
||||||
(filters.duration.from % 3600) / 60,
|
(filters.duration.from % 3600) / 60,
|
||||||
)}m -
|
)}m -
|
||||||
@@ -402,7 +393,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.lessThan}
|
{#if filters.duration.lessThan}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
|
||||||
Duration less than {Math.floor(
|
Duration less than {Math.floor(
|
||||||
filters.duration.lessThan / 3600,
|
filters.duration.lessThan / 3600,
|
||||||
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
|
)}h:{Math.floor((filters.duration.lessThan % 3600) / 60)}m
|
||||||
@@ -410,7 +401,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.duration.moreThan}
|
{#if filters.duration.moreThan}
|
||||||
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
|
<Info icon="stopwatch" onclick={() => (isDurationOpen = true)}>
|
||||||
Duration more than {Math.floor(
|
Duration more than {Math.floor(
|
||||||
filters.duration.moreThan / 3600,
|
filters.duration.moreThan / 3600,
|
||||||
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
|
)}h:{Math.floor((filters.duration.moreThan % 3600) / 60)}m
|
||||||
@@ -418,47 +409,45 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.tags.length != 0}
|
{#if filters.tags.length != 0}
|
||||||
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
|
<Info icon="tags" onclick={() => (isTagsOpen = true)}>
|
||||||
{#each filters.tags as tagId}
|
{#each filters.tags as tagId}
|
||||||
{#key tagId}
|
<Tag id={tagId} clickable={false} />
|
||||||
<Tag id={tagId} clickable={false} />
|
|
||||||
{/key}
|
|
||||||
{/each}
|
{/each}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.numNodes.from != null || filters.numNodes.to != null || filters.numHWThreads.from != null || filters.numHWThreads.to != null || filters.numAccelerators.from != null || filters.numAccelerators.to != null}
|
{#if filters.numNodes.from != null || filters.numNodes.to != null}
|
||||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
<Info icon="hdd-stack" onclick={() => (isResourcesOpen = true)}>
|
||||||
{#if isNodesModified}
|
|
||||||
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
|
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
|
||||||
{/if}
|
</Info>
|
||||||
{#if isNodesModified && isHwthreadsModified},
|
{/if}
|
||||||
{/if}
|
|
||||||
{#if isHwthreadsModified}
|
{#if filters.numHWThreads.from != null || filters.numHWThreads.to != null}
|
||||||
|
<Info icon="cpu" onclick={() => (isResourcesOpen = true)}>
|
||||||
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
|
HWThreads: {filters.numHWThreads.from} - {filters.numHWThreads.to}
|
||||||
{/if}
|
</Info>
|
||||||
{#if (isNodesModified || isHwthreadsModified) && isAccsModified},
|
{/if}
|
||||||
{/if}
|
|
||||||
{#if isAccsModified}
|
{#if filters.numAccelerators.from != null || filters.numAccelerators.to != null}
|
||||||
|
<Info icon="gpu-card" onclick={() => (isResourcesOpen = true)}>
|
||||||
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
|
Accelerators: {filters.numAccelerators.from} - {filters.numAccelerators.to}
|
||||||
{/if}
|
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.node != null}
|
{#if filters.node != null}
|
||||||
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
|
<Info icon="hdd-stack" onclick={() => (isResourcesOpen = true)}>
|
||||||
Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node}
|
Node{nodeMatchLabels[filters.nodeMatch]}: {filters.node}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.energy.from || filters.energy.to}
|
{#if filters.energy.from || filters.energy.to}
|
||||||
<Info icon="lightning-charge-fill" on:click={() => (isEnergyOpen = true)}>
|
<Info icon="lightning-charge-fill" onclick={() => (isEnergyOpen = true)}>
|
||||||
Total Energy: {filters.energy.from} - {filters.energy.to}
|
Total Energy: {filters.energy.from} - {filters.energy.to}
|
||||||
</Info>
|
</Info>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filters.stats.length > 0}
|
{#if filters.stats.length > 0}
|
||||||
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
|
<Info icon="bar-chart" onclick={() => (isStatsOpen = true)}>
|
||||||
{filters.stats
|
{filters.stats
|
||||||
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
|
.map((stat) => `${stat.field}: ${stat.from} - ${stat.to}`)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
@@ -467,69 +456,62 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Cluster
|
<Cluster
|
||||||
{disableClusterSelection}
|
|
||||||
bind:isOpen={isClusterOpen}
|
bind:isOpen={isClusterOpen}
|
||||||
bind:cluster={filters.cluster}
|
presetCluster={filters.cluster}
|
||||||
bind:partition={filters.partition}
|
presetPartition={filters.partition}
|
||||||
on:set-filter={() => updateFilters()}
|
{disableClusterSelection}
|
||||||
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<JobStates
|
<JobStates
|
||||||
bind:isOpen={isJobStatesOpen}
|
bind:isOpen={isJobStatesOpen}
|
||||||
bind:states={filters.states}
|
presetStates={filters.states}
|
||||||
on:set-filter={() => updateFilters()}
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StartTime
|
<StartTime
|
||||||
bind:isOpen={isStartTimeOpen}
|
bind:isOpen={isStartTimeOpen}
|
||||||
bind:from={filters.startTime.from}
|
presetStartTime={filters.startTime}
|
||||||
bind:to={filters.startTime.to}
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
bind:range={filters.startTime.range}
|
|
||||||
{startTimeSelectOptions}
|
|
||||||
on:set-filter={() => updateFilters()}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Duration
|
<Duration
|
||||||
bind:isOpen={isDurationOpen}
|
bind:isOpen={isDurationOpen}
|
||||||
bind:lessThan={filters.duration.lessThan}
|
presetDuration={filters.duration}
|
||||||
bind:moreThan={filters.duration.moreThan}
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
bind:from={filters.duration.from}
|
|
||||||
bind:to={filters.duration.to}
|
|
||||||
on:set-filter={() => updateFilters()}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tags
|
<Tags
|
||||||
bind:isOpen={isTagsOpen}
|
bind:isOpen={isTagsOpen}
|
||||||
bind:tags={filters.tags}
|
presetTags={filters.tags}
|
||||||
on:set-filter={() => updateFilters()}
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Resources
|
<Resources
|
||||||
cluster={filters.cluster}
|
|
||||||
bind:isOpen={isResourcesOpen}
|
bind:isOpen={isResourcesOpen}
|
||||||
bind:numNodes={filters.numNodes}
|
activeCluster={filters.cluster}
|
||||||
bind:numHWThreads={filters.numHWThreads}
|
presetNumNodes={filters.numNodes}
|
||||||
bind:numAccelerators={filters.numAccelerators}
|
presetNumHWThreads={filters.numHWThreads}
|
||||||
bind:namedNode={filters.node}
|
presetNumAccelerators={filters.numAccelerators}
|
||||||
bind:nodeMatch={filters.nodeMatch}
|
presetNamedNode={filters.node}
|
||||||
bind:isNodesModified
|
presetNodeMatch={filters.nodeMatch}
|
||||||
bind:isHwthreadsModified
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
bind:isAccsModified
|
|
||||||
on:set-filter={() => updateFilters()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Statistics
|
|
||||||
bind:isOpen={isStatsOpen}
|
|
||||||
bind:stats={filters.stats}
|
|
||||||
on:set-filter={() => updateFilters()}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Energy
|
<Energy
|
||||||
bind:isOpen={isEnergyOpen}
|
bind:isOpen={isEnergyOpen}
|
||||||
bind:energy={filters.energy}
|
presetEnergy={filters.energy}
|
||||||
on:set-filter={() => updateFilters()}
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Statistics
|
||||||
|
bind:isOpen={isStatsOpen}
|
||||||
|
presetStats={filters.stats}
|
||||||
|
setFilter={(filter) => updateFilters(filter)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
|
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -23,94 +23,91 @@
|
|||||||
import { formatTime, roundTwoDigits } from "./units.js";
|
import { formatTime, roundTwoDigits } from "./units.js";
|
||||||
import Comparogram from "./plots/Comparogram.svelte";
|
import Comparogram from "./plots/Comparogram.svelte";
|
||||||
|
|
||||||
const ccconfig = getContext("cc-config"),
|
/* Svelte 5 Props */
|
||||||
// initialized = getContext("initialized"),
|
let {
|
||||||
globalMetrics = getContext("globalMetrics");
|
matchedCompareJobs = $bindable(0),
|
||||||
|
metrics = $bindable(ccconfig?.plot_list_selectedMetrics),
|
||||||
|
filterBuffer = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let matchedCompareJobs = 0;
|
/* Const Init */
|
||||||
export let metrics = ccconfig.plot_list_selectedMetrics;
|
const client = getContextClient();
|
||||||
export let filterBuffer = [];
|
const ccconfig = getContext("cc-config");
|
||||||
|
const globalMetrics = getContext("globalMetrics");
|
||||||
let filter = [...filterBuffer] || [];
|
// const initialized = getContext("initialized");
|
||||||
let comparePlotData = {};
|
|
||||||
let compareTableData = [];
|
|
||||||
let compareTableSorting = {};
|
|
||||||
let jobIds = [];
|
|
||||||
let jobClusters = [];
|
|
||||||
let tableJobIDFilter = "";
|
|
||||||
|
|
||||||
/*uPlot*/
|
|
||||||
let plotSync = uPlot.sync("compareJobsView");
|
|
||||||
|
|
||||||
/* GQL */
|
|
||||||
const client = getContextClient();
|
|
||||||
// Pull All Series For Metrics Statistics Only On Node Scope
|
// Pull All Series For Metrics Statistics Only On Node Scope
|
||||||
const compareQuery = gql`
|
const compareQuery = gql`
|
||||||
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
|
query ($filter: [JobFilter!]!, $metrics: [String!]!) {
|
||||||
jobsMetricStats(filter: $filter, metrics: $metrics) {
|
jobsMetricStats(filter: $filter, metrics: $metrics) {
|
||||||
id
|
id
|
||||||
jobId
|
jobId
|
||||||
startTime
|
startTime
|
||||||
duration
|
duration
|
||||||
cluster
|
cluster
|
||||||
subCluster
|
subCluster
|
||||||
numNodes
|
numNodes
|
||||||
numHWThreads
|
numHWThreads
|
||||||
numAccelerators
|
numAccelerators
|
||||||
stats {
|
stats {
|
||||||
name
|
name
|
||||||
data {
|
data {
|
||||||
min
|
min
|
||||||
avg
|
avg
|
||||||
max
|
max
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/* REACTIVES */
|
/* Var Init*/
|
||||||
|
let plotSync = uPlot.sync("compareJobsView");
|
||||||
|
|
||||||
$: compareData = queryStore({
|
/* State Init */
|
||||||
client: client,
|
let filter = $state([...filterBuffer] || []);
|
||||||
query: compareQuery,
|
let tableJobIDFilter = $state("");
|
||||||
variables:{ filter, metrics },
|
|
||||||
});
|
|
||||||
|
|
||||||
$: matchedCompareJobs = $compareData.data != null ? $compareData.data.jobsMetricStats.length : -1;
|
/* Derived*/
|
||||||
|
const compareData = $derived(queryStore({
|
||||||
$: if ($compareData.data != null) {
|
client: client,
|
||||||
jobIds = [];
|
query: compareQuery,
|
||||||
jobClusters = [];
|
variables:{ filter, metrics },
|
||||||
comparePlotData = {};
|
})
|
||||||
compareTableData = [...$compareData.data.jobsMetricStats];
|
);
|
||||||
jobs2uplot($compareData.data.jobsMetricStats, metrics);
|
let jobIds = $derived($compareData?.data ? $compareData.data.jobsMetricStats.map((jms) => jms.jobId) : []);
|
||||||
}
|
let jobClusters = $derived($compareData?.data ? $compareData.data.jobsMetricStats.map((jms) => `${jms.cluster} ${jms.subCluster}`) : []);
|
||||||
|
let compareTableData = $derived($compareData?.data ? [...$compareData.data.jobsMetricStats] : []);
|
||||||
$: if ((!$compareData.fetching && !$compareData.error) && metrics) {
|
let comparePlotData = $derived($compareData?.data ? jobs2uplot($compareData.data.jobsMetricStats, metrics) : {});
|
||||||
|
let compareTableSorting = $derived.by(() => {
|
||||||
|
let pendingSort = {};
|
||||||
// Meta
|
// Meta
|
||||||
compareTableSorting['meta'] = {
|
pendingSort['meta'] = {
|
||||||
startTime: { dir: "down", active: true },
|
startTime: { dir: "down", active: true },
|
||||||
duration: { dir: "up", active: false },
|
duration: { dir: "up", active: false },
|
||||||
cluster: { dir: "up", active: false },
|
cluster: { dir: "up", active: false },
|
||||||
};
|
};
|
||||||
// Resources
|
// Resources
|
||||||
compareTableSorting['resources'] = {
|
pendingSort['resources'] = {
|
||||||
Nodes: { dir: "up", active: false },
|
Nodes: { dir: "up", active: false },
|
||||||
Threads: { dir: "up", active: false },
|
Threads: { dir: "up", active: false },
|
||||||
Accs: { dir: "up", active: false },
|
Accs: { dir: "up", active: false },
|
||||||
};
|
};
|
||||||
// Metrics
|
|
||||||
for (let metric of metrics) {
|
for (let metric of metrics) {
|
||||||
compareTableSorting[metric] = {
|
pendingSort[metric] = {
|
||||||
min: { dir: "up", active: false },
|
min: { dir: "up", active: false },
|
||||||
avg: { dir: "up", active: false },
|
avg: { dir: "up", active: false },
|
||||||
max: { dir: "up", active: false },
|
max: { dir: "up", active: false },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
return pendingSort;
|
||||||
|
});
|
||||||
|
|
||||||
/* FUNCTIONS */
|
/* Effect */
|
||||||
|
$effect(() => {
|
||||||
|
matchedCompareJobs = $compareData?.data != null ? $compareData.data.jobsMetricStats.length : -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
// (Re-)query and optionally set new filters; Query will be started reactively.
|
// (Re-)query and optionally set new filters; Query will be started reactively.
|
||||||
export function queryJobs(filters) {
|
export function queryJobs(filters) {
|
||||||
if (filters != null) {
|
if (filters != null) {
|
||||||
@@ -133,6 +130,7 @@
|
|||||||
}
|
}
|
||||||
compareTableSorting = { ...compareTableSorting };
|
compareTableSorting = { ...compareTableSorting };
|
||||||
|
|
||||||
|
let pendingCompareData;
|
||||||
if (key == 'resources') {
|
if (key == 'resources') {
|
||||||
let longField = "";
|
let longField = "";
|
||||||
switch (field) {
|
switch (field) {
|
||||||
@@ -148,12 +146,13 @@
|
|||||||
default:
|
default:
|
||||||
console.log("Unknown Res Field", field)
|
console.log("Unknown Res Field", field)
|
||||||
}
|
}
|
||||||
compareTableData = compareTableData.sort((j1, j2) => {
|
|
||||||
|
pendingCompareData = compareTableData.sort((j1, j2) => {
|
||||||
if (j1[longField] == null || j2[longField] == null) return -1;
|
if (j1[longField] == null || j2[longField] == null) return -1;
|
||||||
return s.dir != "up" ? j1[longField] - j2[longField] : j2[longField] - j1[longField];
|
return s.dir != "up" ? j1[longField] - j2[longField] : j2[longField] - j1[longField];
|
||||||
});
|
});
|
||||||
} else if (key == 'meta') {
|
} else if (key == 'meta') {
|
||||||
compareTableData = compareTableData.sort((j1, j2) => {
|
pendingCompareData = compareTableData.sort((j1, j2) => {
|
||||||
if (j1[field] == null || j2[field] == null) return -1;
|
if (j1[field] == null || j2[field] == null) return -1;
|
||||||
if (field == 'cluster') {
|
if (field == 'cluster') {
|
||||||
let c1 = `${j1.cluster} (${j1.subCluster})`
|
let c1 = `${j1.cluster} (${j1.subCluster})`
|
||||||
@@ -164,52 +163,54 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
compareTableData = compareTableData.sort((j1, j2) => {
|
pendingCompareData = compareTableData.sort((j1, j2) => {
|
||||||
let s1 = j1.stats.find((m) => m.name == key)?.data;
|
let s1 = j1.stats.find((m) => m.name == key)?.data;
|
||||||
let s2 = j2.stats.find((m) => m.name == key)?.data;
|
let s2 = j2.stats.find((m) => m.name == key)?.data;
|
||||||
if (s1 == null || s2 == null) return -1;
|
if (s1 == null || s2 == null) return -1;
|
||||||
return s.dir != "up" ? s1[field] - s2[field] : s2[field] - s1[field];
|
return s.dir != "up" ? s1[field] - s2[field] : s2[field] - s1[field];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compareTableData = [...pendingCompareData]
|
||||||
}
|
}
|
||||||
|
|
||||||
function jobs2uplot(jobs, metrics) {
|
function jobs2uplot(jobs, metrics) {
|
||||||
|
// Proxy Init
|
||||||
|
let pendingComparePlotData = {};
|
||||||
// Resources Init
|
// Resources Init
|
||||||
comparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS]
|
pendingComparePlotData['resources'] = {unit:'', data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YNODES, YTHREADS, YACCS]
|
||||||
// Metric Init
|
// Metric Init
|
||||||
for (let m of metrics) {
|
for (let m of metrics) {
|
||||||
// Get Unit
|
// Get Unit
|
||||||
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
|
const rawUnit = globalMetrics.find((gm) => gm.name == m)?.unit
|
||||||
const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
|
const metricUnit = (rawUnit?.prefix ? rawUnit.prefix : "") + (rawUnit?.base ? rawUnit.base : "")
|
||||||
comparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX]
|
pendingComparePlotData[m] = {unit: metricUnit, data: [[],[],[],[],[],[]]} // data: [X, XST, XRT, YMIN, YAVG, YMAX]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate jobs if exists
|
// Iterate jobs if exists
|
||||||
if (jobs) {
|
if (jobs) {
|
||||||
let plotIndex = 0
|
let plotIndex = 0
|
||||||
jobs.forEach((j) => {
|
jobs.forEach((j) => {
|
||||||
// Collect JobIDs & Clusters for X-Ticks and Legend
|
|
||||||
jobIds.push(j.jobId)
|
|
||||||
jobClusters.push(`${j.cluster} ${j.subCluster}`)
|
|
||||||
// Resources
|
// Resources
|
||||||
comparePlotData['resources'].data[0].push(plotIndex)
|
pendingComparePlotData['resources'].data[0].push(plotIndex)
|
||||||
comparePlotData['resources'].data[1].push(j.startTime)
|
pendingComparePlotData['resources'].data[1].push(j.startTime)
|
||||||
comparePlotData['resources'].data[2].push(j.duration)
|
pendingComparePlotData['resources'].data[2].push(j.duration)
|
||||||
comparePlotData['resources'].data[3].push(j.numNodes)
|
pendingComparePlotData['resources'].data[3].push(j.numNodes)
|
||||||
comparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
|
pendingComparePlotData['resources'].data[4].push(j?.numHWThreads?j.numHWThreads:0)
|
||||||
comparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
|
pendingComparePlotData['resources'].data[5].push(j?.numAccelerators?j.numAccelerators:0)
|
||||||
// Metrics
|
// Metrics
|
||||||
for (let s of j.stats) {
|
for (let s of j.stats) {
|
||||||
comparePlotData[s.name].data[0].push(plotIndex)
|
pendingComparePlotData[s.name].data[0].push(plotIndex)
|
||||||
comparePlotData[s.name].data[1].push(j.startTime)
|
pendingComparePlotData[s.name].data[1].push(j.startTime)
|
||||||
comparePlotData[s.name].data[2].push(j.duration)
|
pendingComparePlotData[s.name].data[2].push(j.duration)
|
||||||
comparePlotData[s.name].data[3].push(s.data.min)
|
pendingComparePlotData[s.name].data[3].push(s.data.min)
|
||||||
comparePlotData[s.name].data[4].push(s.data.avg)
|
pendingComparePlotData[s.name].data[4].push(s.data.avg)
|
||||||
comparePlotData[s.name].data[5].push(s.data.max)
|
pendingComparePlotData[s.name].data[5].push(s.data.max)
|
||||||
}
|
}
|
||||||
plotIndex++
|
plotIndex++
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return {...pendingComparePlotData};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapt for Persisting Job Selections in DB later down the line
|
// Adapt for Persisting Job Selections in DB later down the line
|
||||||
@@ -238,7 +239,6 @@
|
|||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $compareData.fetching}
|
{#if $compareData.fetching}
|
||||||
@@ -265,7 +265,7 @@
|
|||||||
xticks={jobIds}
|
xticks={jobIds}
|
||||||
xinfo={jobClusters}
|
xinfo={jobClusters}
|
||||||
ylabel={'Resource Counts'}
|
ylabel={'Resource Counts'}
|
||||||
data={comparePlotData['resources'].data}
|
data={comparePlotData['resources']?.data}
|
||||||
{plotSync}
|
{plotSync}
|
||||||
forResources
|
forResources
|
||||||
/>
|
/>
|
||||||
@@ -281,8 +281,8 @@
|
|||||||
xinfo={jobClusters}
|
xinfo={jobClusters}
|
||||||
ylabel={m}
|
ylabel={m}
|
||||||
metric={m}
|
metric={m}
|
||||||
yunit={comparePlotData[m].unit}
|
yunit={comparePlotData[m]?.unit}
|
||||||
data={comparePlotData[m].data}
|
data={comparePlotData[m]?.data}
|
||||||
{plotSync}
|
{plotSync}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</th>
|
</th>
|
||||||
<th on:click={() => sortBy('meta', 'startTime')}>
|
<th onclick={() => sortBy('meta', 'startTime')}>
|
||||||
Sort
|
Sort
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
|
name="caret-{compareTableSorting['meta']['startTime'].dir}{compareTableSorting['meta']['startTime']
|
||||||
@@ -323,7 +323,7 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th on:click={() => sortBy('meta', 'duration')}>
|
<th onclick={() => sortBy('meta', 'duration')}>
|
||||||
Sort
|
Sort
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
|
name="caret-{compareTableSorting['meta']['duration'].dir}{compareTableSorting['meta']['duration']
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th on:click={() => sortBy('meta', 'cluster')}>
|
<th onclick={() => sortBy('meta', 'cluster')}>
|
||||||
Sort
|
Sort
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
|
name="caret-{compareTableSorting['meta']['cluster'].dir}{compareTableSorting['meta']['cluster']
|
||||||
@@ -342,7 +342,7 @@
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
{#each ["Nodes", "Threads", "Accs"] as res}
|
{#each ["Nodes", "Threads", "Accs"] as res}
|
||||||
<th on:click={() => sortBy('resources', res)}>
|
<th onclick={() => sortBy('resources', res)}>
|
||||||
{res}
|
{res}
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
|
name="caret-{compareTableSorting['resources'][res].dir}{compareTableSorting['resources'][res]
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{#each metrics as metric}
|
{#each metrics as metric}
|
||||||
{#each ["min", "avg", "max"] as stat}
|
{#each ["min", "avg", "max"] as stat}
|
||||||
<th on:click={() => sortBy(metric, stat)}>
|
<th onclick={() => sortBy(metric, stat)}>
|
||||||
{stat.charAt(0).toUpperCase() + stat.slice(1)}
|
{stat.charAt(0).toUpperCase() + stat.slice(1)}
|
||||||
<Icon
|
<Icon
|
||||||
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]
|
name="caret-{compareTableSorting[metric][stat].dir}{compareTableSorting[metric][stat]
|
||||||
|
|||||||
@@ -113,11 +113,6 @@
|
|||||||
variables: { paging, sorting, filter },
|
variables: { paging, sorting, filter },
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (!usePaging && sorting) {
|
|
||||||
// console.log('Reset Paging ...')
|
|
||||||
paging = { itemsPerPage: 10, page: 1 }
|
|
||||||
};
|
|
||||||
|
|
||||||
let jobs = [];
|
let jobs = [];
|
||||||
$: if ($initialized && $jobsStore.data) {
|
$: if ($initialized && $jobsStore.data) {
|
||||||
if (usePaging) {
|
if (usePaging) {
|
||||||
@@ -143,10 +138,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (!usePaging && (sorting || filter)) {
|
||||||
|
// Continous Scroll: Reset list and paging if parameters change: Existing entries will not match new selections
|
||||||
|
jobs = [];
|
||||||
|
paging = { itemsPerPage: 10, page: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
$: matchedListJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
|
$: matchedListJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : -1;
|
||||||
|
|
||||||
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
|
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
|
||||||
export function refreshJobs() {
|
export function refreshJobs() {
|
||||||
|
if (!usePaging) {
|
||||||
|
jobs = []; // Empty Joblist before refresh, prevents infinite buildup
|
||||||
|
paging = { itemsPerPage: 10, page: 1 };
|
||||||
|
}
|
||||||
jobsStore = queryStore({
|
jobsStore = queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: query,
|
query: query,
|
||||||
@@ -285,7 +290,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{:else}
|
{:else}
|
||||||
{#each jobs as job (job)}
|
{#each jobs as job (job.id)}
|
||||||
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
|
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} previousSelect={selectedJobs.includes(job.id)}
|
||||||
on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]}
|
on:select-job={({detail}) => selectedJobs = [...selectedJobs, detail]}
|
||||||
on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)}
|
on:unselect-job={({detail}) => selectedJobs = selectedJobs.filter(item => item !== detail)}
|
||||||
@@ -312,11 +317,11 @@
|
|||||||
|
|
||||||
{#if usePaging}
|
{#if usePaging}
|
||||||
<Pagination
|
<Pagination
|
||||||
bind:page
|
{page}
|
||||||
{itemsPerPage}
|
{itemsPerPage}
|
||||||
itemText="Jobs"
|
itemText="Jobs"
|
||||||
totalItems={matchedListJobs}
|
totalItems={matchedListJobs}
|
||||||
on:update-paging={({ detail }) => {
|
updatePaging={(detail) => {
|
||||||
if (detail.itemsPerPage != itemsPerPage) {
|
if (detail.itemsPerPage != itemsPerPage) {
|
||||||
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,6 +14,18 @@
|
|||||||
|
|
||||||
export let itemsPerRow
|
export let itemsPerRow
|
||||||
export let items
|
export let items
|
||||||
|
|
||||||
|
/* Migtation Notes
|
||||||
|
* Requirements
|
||||||
|
* - Parent Components must be already Migrated
|
||||||
|
* - TODO: Job.root.svelte, Node.root.svelte
|
||||||
|
* - DONE: Analysis, Status, User
|
||||||
|
*
|
||||||
|
* How-To
|
||||||
|
* - Define "Plot-Slotcode" as SV5 Snippet with argument "item" in parent (!)
|
||||||
|
* - Pass new snippet as argument/prop to here
|
||||||
|
* - @render snippet in items-loop with argument == item
|
||||||
|
*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}>
|
<Row cols={{ xs: 1, sm: 2, md: 3, lg: itemsPerRow}}>
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
<!--
|
|
||||||
@component Organized display of plots as table
|
|
||||||
|
|
||||||
Properties:
|
|
||||||
- `itemsPerRow Number`: Elements to render per row
|
|
||||||
- `items [Any]`: List of plot components to render
|
|
||||||
- `padding Number`: Padding between plot elements
|
|
||||||
- `renderFor String`: If 'job', filter disabled metrics
|
|
||||||
-->
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export let itemsPerRow
|
|
||||||
export let items
|
|
||||||
export let padding = 10
|
|
||||||
export let renderFor
|
|
||||||
|
|
||||||
let rows = []
|
|
||||||
let tableWidth = 0
|
|
||||||
const isPlaceholder = x => x._is_placeholder === true
|
|
||||||
|
|
||||||
function tile(items, itemsPerRow) {
|
|
||||||
const rows = []
|
|
||||||
for (let ri = 0; ri < items.length; ri += itemsPerRow) {
|
|
||||||
const row = []
|
|
||||||
for (let ci = 0; ci < itemsPerRow; ci += 1) {
|
|
||||||
if (ri + ci < items.length)
|
|
||||||
row.push(items[ri + ci])
|
|
||||||
else
|
|
||||||
row.push({ _is_placeholder: true, ri, ci })
|
|
||||||
}
|
|
||||||
rows.push(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$: if (renderFor === 'job') {
|
|
||||||
rows = tile(items.filter(item => item.disabled === false), itemsPerRow)
|
|
||||||
} else {
|
|
||||||
rows = tile(items, itemsPerRow)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: plotWidth = (tableWidth / itemsPerRow) - (padding * itemsPerRow)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<table bind:clientWidth={tableWidth} style="width: 100%; table-layout: fixed;">
|
|
||||||
{#each rows as row}
|
|
||||||
<tr>
|
|
||||||
{#each row as item (item)}
|
|
||||||
<td style="vertical-align:top;"> <!-- For Aligning Notice Cards -->
|
|
||||||
{#if !isPlaceholder(item) && plotWidth > 0}
|
|
||||||
<slot item={item} width={plotWidth}></slot>
|
|
||||||
{/if}
|
|
||||||
</td>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</table>
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ListGroup,
|
ListGroup,
|
||||||
@@ -24,18 +24,23 @@
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const clusters = getContext("clusters"),
|
/* Svelte 5 Props */
|
||||||
initialized = getContext("initialized"),
|
let {
|
||||||
dispatch = createEventDispatcher();
|
isOpen = $bindable(false),
|
||||||
|
presetCluster = "",
|
||||||
|
presetPartition = "",
|
||||||
|
disableClusterSelection = false,
|
||||||
|
setFilter
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let pendingCluster = $state(presetCluster);
|
||||||
|
let pendingPartition = $state(presetPartition);
|
||||||
|
|
||||||
|
/* Derived Vars */
|
||||||
|
const clusters = $derived(getContext("clusters"));
|
||||||
|
const initialized = $derived(getContext("initialized"));
|
||||||
|
|
||||||
export let disableClusterSelection = false;
|
|
||||||
export let isModified = false;
|
|
||||||
export let isOpen = false;
|
|
||||||
export let cluster = null;
|
|
||||||
export let partition = null;
|
|
||||||
let pendingCluster = cluster,
|
|
||||||
pendingPartition = partition;
|
|
||||||
$: isModified = pendingCluster != cluster || pendingPartition != partition;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
@@ -45,13 +50,13 @@
|
|||||||
<h4>Cluster</h4>
|
<h4>Cluster</h4>
|
||||||
{#if disableClusterSelection}
|
{#if disableClusterSelection}
|
||||||
<Button color="info" class="w-100 mb-2" disabled><b>Info: Cluster Selection Disabled in This View</b></Button>
|
<Button color="info" class="w-100 mb-2" disabled><b>Info: Cluster Selection Disabled in This View</b></Button>
|
||||||
<Button outline color="primary" class="w-100 mb-2" disabled><b>Selected Cluster: {cluster}</b></Button>
|
<Button outline color="primary" class="w-100 mb-2" disabled><b>Selected Cluster: {presetCluster}</b></Button>
|
||||||
{:else}
|
{:else}
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
disabled={disableClusterSelection}
|
disabled={disableClusterSelection}
|
||||||
active={pendingCluster == null}
|
active={pendingCluster == null}
|
||||||
on:click={() => ((pendingCluster = null), (pendingPartition = null))}
|
onclick={() => ((pendingCluster = null), (pendingPartition = null))}
|
||||||
>
|
>
|
||||||
Any Cluster
|
Any Cluster
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
@@ -59,7 +64,7 @@
|
|||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
disabled={disableClusterSelection}
|
disabled={disableClusterSelection}
|
||||||
active={pendingCluster == cluster.name}
|
active={pendingCluster == cluster.name}
|
||||||
on:click={() => (
|
onclick={() => (
|
||||||
(pendingCluster = cluster.name), (pendingPartition = null)
|
(pendingCluster = cluster.name), (pendingPartition = null)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -75,14 +80,14 @@
|
|||||||
<ListGroup>
|
<ListGroup>
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
active={pendingPartition == null}
|
active={pendingPartition == null}
|
||||||
on:click={() => (pendingPartition = null)}
|
onclick={() => (pendingPartition = null)}
|
||||||
>
|
>
|
||||||
Any Partition
|
Any Partition
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
{#each clusters.find((c) => c.name == pendingCluster).partitions as partition}
|
{#each clusters?.find((c) => c.name == pendingCluster)?.partitions as partition}
|
||||||
<ListGroupItem
|
<ListGroupItem
|
||||||
active={pendingPartition == partition}
|
active={pendingPartition == partition}
|
||||||
on:click={() => (pendingPartition = partition)}
|
onclick={() => (pendingPartition = partition)}
|
||||||
>
|
>
|
||||||
{partition}
|
{partition}
|
||||||
</ListGroupItem>
|
</ListGroupItem>
|
||||||
@@ -93,22 +98,22 @@
|
|||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
cluster = pendingCluster;
|
setFilter({ cluster: pendingCluster, partition: pendingPartition });
|
||||||
partition = pendingPartition;
|
|
||||||
dispatch("set-filter", { cluster, partition });
|
|
||||||
}}>Close & Apply</Button
|
}}>Close & Apply</Button
|
||||||
>
|
>
|
||||||
<Button
|
{#if !disableClusterSelection}
|
||||||
color="danger"
|
<Button
|
||||||
on:click={() => {
|
color="danger"
|
||||||
isOpen = false;
|
onclick={() => {
|
||||||
cluster = pendingCluster = null;
|
isOpen = false;
|
||||||
partition = pendingPartition = null;
|
pendingCluster = null;
|
||||||
dispatch("set-filter", { cluster, partition });
|
pendingPartition = null;
|
||||||
}}>Reset</Button
|
setFilter({ cluster: pendingCluster, partition: pendingPartition})
|
||||||
>
|
}}>Reset</Button
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
>
|
||||||
|
{/if}
|
||||||
|
<Button onclick={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
@@ -24,61 +23,81 @@
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
isOpen = $bindable(false),
|
||||||
|
presetDuration ={lessThan: null, moreThan: null, from: null, to: null},
|
||||||
|
setFilter
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let isOpen = false;
|
/* State Init */
|
||||||
export let lessThan = null;
|
let pendingDuration = $state(presetDuration);
|
||||||
export let moreThan = null;
|
let lessState = $state(secsToHoursAndMins(presetDuration?.lessThan));
|
||||||
export let from = null;
|
let moreState = $state(secsToHoursAndMins(presetDuration?.moreThan));
|
||||||
export let to = null;
|
let fromState = $state(secsToHoursAndMins(presetDuration?.from));
|
||||||
|
let toState = $state(secsToHoursAndMins(presetDuration?.to));
|
||||||
|
|
||||||
let pendingLessThan, pendingMoreThan, pendingFrom, pendingTo;
|
/* Derived Init */
|
||||||
let lessDisabled = false,
|
const lessDisabled = $derived(
|
||||||
moreDisabled = false,
|
moreState.hours !== 0 ||
|
||||||
betweenDisabled = false;
|
moreState.mins !== 0 ||
|
||||||
|
fromState.hours !== 0 ||
|
||||||
|
fromState.mins !== 0 ||
|
||||||
|
toState.hours !== 0 ||
|
||||||
|
toState.mins !== 0
|
||||||
|
);
|
||||||
|
|
||||||
function reset() {
|
const moreDisabled = $derived(
|
||||||
pendingLessThan =
|
lessState.hours !== 0 ||
|
||||||
lessThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(lessThan);
|
lessState.mins !== 0 ||
|
||||||
pendingMoreThan =
|
fromState.hours !== 0 ||
|
||||||
moreThan == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(moreThan);
|
fromState.mins !== 0 ||
|
||||||
pendingFrom =
|
toState.hours !== 0 ||
|
||||||
from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from);
|
toState.mins !== 0
|
||||||
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to);
|
);
|
||||||
|
|
||||||
|
const betweenDisabled = $derived(
|
||||||
|
moreState.hours !== 0 ||
|
||||||
|
moreState.mins !== 0 ||
|
||||||
|
lessState.hours !== 0 ||
|
||||||
|
lessState.mins !== 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function resetPending() {
|
||||||
|
pendingDuration = {
|
||||||
|
lessThan: null,
|
||||||
|
moreThan: null,
|
||||||
|
from: null,
|
||||||
|
to: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function resetStates() {
|
||||||
|
lessState = { hours: 0, mins: 0 }
|
||||||
|
moreState = { hours: 0, mins: 0 }
|
||||||
|
fromState = { hours: 0, mins: 0 }
|
||||||
|
toState = { hours: 0, mins: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
function secsToHoursAndMins(seconds) {
|
||||||
|
if (seconds) {
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
seconds -= hours * 3600;
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
return { hours, mins };
|
||||||
|
} else {
|
||||||
|
return { hours: 0, mins: 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reset();
|
function hoursAndMinsToSecs(hoursAndMins) {
|
||||||
|
if (hoursAndMins) {
|
||||||
function secsToHoursAndMins(duration) {
|
return hoursAndMins.hours * 3600 + hoursAndMins.mins * 60;
|
||||||
const hours = Math.floor(duration / 3600);
|
} else {
|
||||||
duration -= hours * 3600;
|
return 0
|
||||||
const mins = Math.floor(duration / 60);
|
}
|
||||||
return { hours, mins };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hoursAndMinsToSecs({ hours, mins }) {
|
|
||||||
return hours * 3600 + mins * 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
$: lessDisabled =
|
|
||||||
pendingMoreThan.hours !== 0 ||
|
|
||||||
pendingMoreThan.mins !== 0 ||
|
|
||||||
pendingFrom.hours !== 0 ||
|
|
||||||
pendingFrom.mins !== 0 ||
|
|
||||||
pendingTo.hours !== 0 ||
|
|
||||||
pendingTo.mins !== 0;
|
|
||||||
$: moreDisabled =
|
|
||||||
pendingLessThan.hours !== 0 ||
|
|
||||||
pendingLessThan.mins !== 0 ||
|
|
||||||
pendingFrom.hours !== 0 ||
|
|
||||||
pendingFrom.mins !== 0 ||
|
|
||||||
pendingTo.hours !== 0 ||
|
|
||||||
pendingTo.mins !== 0;
|
|
||||||
$: betweenDisabled =
|
|
||||||
pendingMoreThan.hours !== 0 ||
|
|
||||||
pendingMoreThan.mins !== 0 ||
|
|
||||||
pendingLessThan.hours !== 0 ||
|
|
||||||
pendingLessThan.mins !== 0;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
@@ -92,7 +111,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={pendingMoreThan.hours}
|
bind:value={moreState.hours}
|
||||||
disabled={moreDisabled}
|
disabled={moreDisabled}
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -107,7 +126,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="59"
|
max="59"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={pendingMoreThan.mins}
|
bind:value={moreState.mins}
|
||||||
disabled={moreDisabled}
|
disabled={moreDisabled}
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -126,7 +145,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={pendingLessThan.hours}
|
bind:value={lessState.hours}
|
||||||
disabled={lessDisabled}
|
disabled={lessDisabled}
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -141,7 +160,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="59"
|
max="59"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={pendingLessThan.mins}
|
bind:value={lessState.mins}
|
||||||
disabled={lessDisabled}
|
disabled={lessDisabled}
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -160,7 +179,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={pendingFrom.hours}
|
bind:value={fromState.hours}
|
||||||
disabled={betweenDisabled}
|
disabled={betweenDisabled}
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -175,7 +194,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="59"
|
max="59"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={pendingFrom.mins}
|
bind:value={fromState.mins}
|
||||||
disabled={betweenDisabled}
|
disabled={betweenDisabled}
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -192,7 +211,7 @@
|
|||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={pendingTo.hours}
|
bind:value={toState.hours}
|
||||||
disabled={betweenDisabled}
|
disabled={betweenDisabled}
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -207,7 +226,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="59"
|
max="59"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
bind:value={pendingTo.mins}
|
bind:value={toState.mins}
|
||||||
disabled={betweenDisabled}
|
disabled={betweenDisabled}
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@@ -220,39 +239,32 @@
|
|||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
lessThan = hoursAndMinsToSecs(pendingLessThan);
|
pendingDuration.lessThan = hoursAndMinsToSecs(lessState);
|
||||||
moreThan = hoursAndMinsToSecs(pendingMoreThan);
|
pendingDuration.moreThan = hoursAndMinsToSecs(moreState);
|
||||||
from = hoursAndMinsToSecs(pendingFrom);
|
pendingDuration.from = hoursAndMinsToSecs(fromState);
|
||||||
to = hoursAndMinsToSecs(pendingTo);
|
pendingDuration.to = hoursAndMinsToSecs(toState);
|
||||||
dispatch("set-filter", { lessThan, moreThan, from, to });
|
setFilter({duration: pendingDuration});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Close & Apply
|
Close & Apply
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
lessThan = null;
|
resetStates();
|
||||||
moreThan = null;
|
|
||||||
from = null;
|
|
||||||
to = null;
|
|
||||||
reset();
|
|
||||||
}}>Reset Values</Button
|
}}>Reset Values</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
lessThan = null;
|
resetStates();
|
||||||
moreThan = null;
|
resetPending();
|
||||||
from = null;
|
setFilter({duration: pendingDuration});
|
||||||
to = null;
|
|
||||||
reset();
|
|
||||||
dispatch("set-filter", { lessThan, moreThan, from, to });
|
|
||||||
}}>Reset Filter</Button
|
}}>Reset Filter</Button
|
||||||
>
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button onclick={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -20,49 +19,49 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
|
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
isOpen = $bindable(false),
|
||||||
|
presetEnergy= {from: null, to: null},
|
||||||
|
setFilter,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const energyMaximum = 1000.0;
|
/* State Init */
|
||||||
|
let energyState = $state(presetEnergy);
|
||||||
|
|
||||||
export let isOpen = false;
|
|
||||||
export let energy= {from: null, to: null};
|
|
||||||
|
|
||||||
function resetRanges() {
|
|
||||||
energy.from = null
|
|
||||||
energy.to = null
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>Filter based on energy</ModalHeader>
|
<ModalHeader>Filter based on energy</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h4>Total Job Energy (kWh)</h4>
|
<div class="mb-3">
|
||||||
<DoubleRangeSlider
|
<div class="mb-0"><b>Total Job Energy (kWh)</b></div>
|
||||||
on:change={({ detail }) => (
|
<DoubleRangeSlider
|
||||||
(energy.from = detail[0]), (energy.to = detail[1])
|
changeRange={(detail) => {
|
||||||
)}
|
energyState.from = detail[0];
|
||||||
min={0.0}
|
energyState.to = detail[1];
|
||||||
max={energyMaximum}
|
}}
|
||||||
firstSlider={energy?.from ? energy.from : 0.0}
|
sliderMin={0.0}
|
||||||
secondSlider={energy?.to ? energy.to : energyMaximum}
|
sliderMax={1000.0}
|
||||||
inputFieldFrom={energy?.from ? energy.from : null}
|
fromPreset={energyState?.from? energyState.from : 0.0}
|
||||||
inputFieldTo={energy?.to ? energy.to : null}
|
toPreset={energyState?.to? energyState.to : 1000.0}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
dispatch("set-filter", { energy });
|
setFilter({ energy: energyState });
|
||||||
}}>Close & Apply</Button
|
}}>Close & Apply</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
resetRanges();
|
energyState = {from: null, to: null};
|
||||||
dispatch("set-filter", { energy });
|
setFilter({ energy: energyState });
|
||||||
}}>Reset</Button
|
}}>Reset</Button
|
||||||
>
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||||
|
|||||||
@@ -4,16 +4,22 @@
|
|||||||
Properties:
|
Properties:
|
||||||
- `icon String`: Sveltestrap icon name
|
- `icon String`: Sveltestrap icon name
|
||||||
- `modified Bool?`: Optional if filter is modified [Default: false]
|
- `modified Bool?`: Optional if filter is modified [Default: false]
|
||||||
|
- `onclick Fn()`: Opens Modal on click
|
||||||
|
- `children Fn()?`: Internal prop, Svelte 5 version of <slot/>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Button, Icon } from "@sveltestrap/sveltestrap";
|
import { Button, Icon } from "@sveltestrap/sveltestrap";
|
||||||
|
/* Svelte 5 Props */
|
||||||
export let icon;
|
let { icon, modified, onclick, children } = $props();
|
||||||
export let modified = false;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} on:click>
|
<Button class="mr-2 mb-1" outline color={modified ? "warning" : "primary"} {onclick}>
|
||||||
<Icon name={icon} />
|
<Icon name={icon} />
|
||||||
<slot />
|
{#if children}
|
||||||
|
<!-- Note: Ignore '@' Error in IDE -->
|
||||||
|
{@render children()}
|
||||||
|
{:else}
|
||||||
|
<span>No content found</span>
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
- `const allJobStates [String]`: List of all available job states used in cc-backend
|
- `const allJobStates [String]`: List of all available job states used in cc-backend
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script context="module">
|
<script module>
|
||||||
export const allJobStates = [
|
export const allJobStates = [
|
||||||
"running",
|
"running",
|
||||||
"completed",
|
"completed",
|
||||||
@@ -27,7 +27,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
/* Note: Ignore VSCode reported 'A component can only have one instance-level <script> element' error */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ListGroup,
|
ListGroup,
|
||||||
@@ -38,16 +39,16 @@
|
|||||||
ModalFooter,
|
ModalFooter,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
isOpen = $bindable(false),
|
||||||
|
presetStates = [...allJobStates],
|
||||||
|
setFilter
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let isModified = false;
|
/* State Init */
|
||||||
export let isOpen = false;
|
let pendingStates = $state([...presetStates]);
|
||||||
export let states = [...allJobStates];
|
|
||||||
|
|
||||||
let pendingStates = [...states];
|
|
||||||
$: isModified =
|
|
||||||
states.length != pendingStates.length ||
|
|
||||||
!states.every((state) => pendingStates.includes(state));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
@@ -71,28 +72,25 @@
|
|||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={pendingStates.length == 0}
|
disabled={pendingStates.length == 0}
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
states = [...pendingStates];
|
setFilter({ states: [...pendingStates] });
|
||||||
dispatch("set-filter", { states });
|
|
||||||
}}>Close & Apply</Button
|
}}>Close & Apply</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
states = [...allJobStates];
|
|
||||||
pendingStates = [];
|
pendingStates = [];
|
||||||
}}>Deselect All</Button
|
}}>Deselect All</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
states = [...allJobStates];
|
|
||||||
pendingStates = [...allJobStates];
|
pendingStates = [...allJobStates];
|
||||||
dispatch("set-filter", { states });
|
setFilter({ states: [...pendingStates] });
|
||||||
}}>Reset</Button
|
}}>Reset</Button
|
||||||
>
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button onclick={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
@component Filter sub-component for selecting job resources
|
@component Filter sub-component for selecting job resources
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `cluster Object?`: The currently selected cluster config [Default: null]
|
|
||||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||||
|
- `activeCluster String?`: The currently selected cluster name [Default: null]
|
||||||
- `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}]
|
- `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}]
|
||||||
- `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}]
|
- `numHWThreads Object?`: The currently selected numHWThreads filter [Default: {from:null, to:null}]
|
||||||
- `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}]
|
- `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}]
|
||||||
- `isNodesModified Bool?`: Is the node filter modified [Default: false]
|
|
||||||
- `isHwthreadsModified Bool?`: Is the Hwthreads filter modified [Default: false]
|
|
||||||
- `isAccsModified Bool?`: Is the Accelerator filter modified [Default: false]
|
|
||||||
- `namedNode String?`: The currently selected single named node (= hostname) [Default: null]
|
- `namedNode String?`: The currently selected single named node (= hostname) [Default: null]
|
||||||
|
|
||||||
Events:
|
Events:
|
||||||
@@ -17,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -28,27 +25,19 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
|
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
|
||||||
|
|
||||||
const clusters = getContext("clusters"),
|
/* Svelte 5 Props*/
|
||||||
initialized = getContext("initialized"),
|
let {
|
||||||
dispatch = createEventDispatcher();
|
isOpen = $bindable(false),
|
||||||
|
activeCluster = null,
|
||||||
export let cluster = null;
|
presetNumNodes = { from: null, to: null },
|
||||||
export let isOpen = false;
|
presetNumHWThreads = { from: null, to: null },
|
||||||
export let numNodes = { from: null, to: null };
|
presetNumAccelerators = { from: null, to: null },
|
||||||
export let numHWThreads = { from: null, to: null };
|
presetNamedNode = null,
|
||||||
export let numAccelerators = { from: null, to: null };
|
presetNodeMatch = "eq",
|
||||||
export let isNodesModified = false;
|
setFilter
|
||||||
export let isHwthreadsModified = false;
|
} = $props()
|
||||||
export let isAccsModified = false;
|
|
||||||
export let namedNode = null;
|
|
||||||
export let nodeMatch = "eq"
|
|
||||||
|
|
||||||
let pendingNumNodes = numNodes,
|
|
||||||
pendingNumHWThreads = numHWThreads,
|
|
||||||
pendingNumAccelerators = numAccelerators,
|
|
||||||
pendingNamedNode = namedNode,
|
|
||||||
pendingNodeMatch = nodeMatch;
|
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const nodeMatchLabels = {
|
const nodeMatchLabels = {
|
||||||
eq: "Equal To",
|
eq: "Equal To",
|
||||||
contains: "Contains",
|
contains: "Contains",
|
||||||
@@ -85,75 +74,133 @@
|
|||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
let minNumNodes = 1,
|
/* State Init*/
|
||||||
maxNumNodes = 0,
|
// Counts
|
||||||
minNumHWThreads = 1,
|
let minNumNodes = $state(1);
|
||||||
maxNumHWThreads = 0,
|
let maxNumNodes = $state(0);
|
||||||
minNumAccelerators = 0,
|
let maxNumHWThreads = $state(0);
|
||||||
maxNumAccelerators = 0;
|
let maxNumAccelerators = $state(0);
|
||||||
$: {
|
// Pending
|
||||||
|
let pendingNumNodes = $state(presetNumNodes);
|
||||||
|
let pendingNumHWThreads = $state(presetNumHWThreads);
|
||||||
|
let pendingNumAccelerators = $state(presetNumAccelerators);
|
||||||
|
let pendingNamedNode = $state(presetNamedNode);
|
||||||
|
let pendingNodeMatch = $state(presetNodeMatch);
|
||||||
|
// Changable States
|
||||||
|
let nodesState = $state(presetNumNodes);
|
||||||
|
let threadState = $state(presetNumHWThreads);
|
||||||
|
let accState = $state(presetNumAccelerators);
|
||||||
|
|
||||||
|
/* Derived States */
|
||||||
|
const clusters = $derived(getContext("clusters"));
|
||||||
|
const initialized = $derived(getContext("initialized"));
|
||||||
|
// Is Selection Active
|
||||||
|
const nodesActive = $derived(!(JSON.stringify(nodesState) === JSON.stringify({ from: 1, to: maxNumNodes })));
|
||||||
|
const threadActive = $derived(!(JSON.stringify(threadState) === JSON.stringify({ from: 1, to: maxNumHWThreads })));
|
||||||
|
const accActive = $derived(!(JSON.stringify(accState) === JSON.stringify({ from: 0, to: maxNumAccelerators })));
|
||||||
|
// Block Apply if null
|
||||||
|
const disableApply = $derived(
|
||||||
|
nodesState.from === null || nodesState.to === null ||
|
||||||
|
threadState.from === null || threadState.to === null ||
|
||||||
|
accState.from === null || accState.to === null
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Reactive Effects | Svelte 5 onMount */
|
||||||
|
$effect(() => {
|
||||||
if ($initialized) {
|
if ($initialized) {
|
||||||
if (cluster != null) {
|
// 'hClusters' defined in templates/base.tmpl
|
||||||
const { subClusters } = clusters.find((c) => c.name == cluster);
|
if (activeCluster != null) {
|
||||||
const { filterRanges } = header.clusters.find((c) => c.name == cluster);
|
const { filterRanges } = hClusters.find((c) => c.name == activeCluster);
|
||||||
minNumNodes = filterRanges.numNodes.from;
|
minNumNodes = filterRanges.numNodes.from;
|
||||||
maxNumNodes = filterRanges.numNodes.to;
|
maxNumNodes = filterRanges.numNodes.to;
|
||||||
|
} else if (clusters.length > 0) {
|
||||||
|
for (let hc of hClusters) {
|
||||||
|
const { filterRanges } = hc;
|
||||||
|
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
|
||||||
|
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($initialized) {
|
||||||
|
// 'hClusters' defined in templates/base.tmpl
|
||||||
|
if (activeCluster != null) {
|
||||||
|
const { subClusters } = clusters.find((c) => c.name == activeCluster);
|
||||||
maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
|
maxNumAccelerators = findMaxNumAccels([{ subClusters }]);
|
||||||
maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]);
|
maxNumHWThreads = findMaxNumHWThreadsPerNode([{ subClusters }]);
|
||||||
} else if (clusters.length > 0) {
|
} else if (clusters.length > 0) {
|
||||||
const { filterRanges } = header.clusters[0];
|
|
||||||
minNumNodes = filterRanges.numNodes.from;
|
|
||||||
maxNumNodes = filterRanges.numNodes.to;
|
|
||||||
maxNumAccelerators = findMaxNumAccels(clusters);
|
maxNumAccelerators = findMaxNumAccels(clusters);
|
||||||
maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters);
|
maxNumHWThreads = findMaxNumHWThreadsPerNode(clusters);
|
||||||
for (let cluster of header.clusters) {
|
|
||||||
const { filterRanges } = cluster;
|
|
||||||
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from);
|
|
||||||
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
$: {
|
$effect(() => {
|
||||||
if (
|
if (
|
||||||
isOpen &&
|
|
||||||
$initialized &&
|
$initialized &&
|
||||||
pendingNumNodes.from == null &&
|
pendingNumNodes.from == null &&
|
||||||
pendingNumNodes.to == null
|
pendingNumNodes.to == null
|
||||||
) {
|
) {
|
||||||
pendingNumNodes = { from: 0, to: maxNumNodes };
|
nodesState = { from: 1, to: maxNumNodes };
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
$: {
|
$effect(() => {
|
||||||
if (
|
if (
|
||||||
isOpen &&
|
|
||||||
$initialized &&
|
$initialized &&
|
||||||
((pendingNumHWThreads.from == null && pendingNumHWThreads.to == null) ||
|
pendingNumHWThreads.from == null &&
|
||||||
isHwthreadsModified == false)
|
pendingNumHWThreads.to == null
|
||||||
) {
|
) {
|
||||||
pendingNumHWThreads = { from: 0, to: maxNumHWThreads };
|
threadState = { from: 1, to: maxNumHWThreads };
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
$: if (maxNumAccelerators != null && maxNumAccelerators > 1) {
|
$effect(() => {
|
||||||
if (
|
if (
|
||||||
isOpen &&
|
|
||||||
$initialized &&
|
$initialized &&
|
||||||
pendingNumAccelerators.from == null &&
|
pendingNumAccelerators.from == null &&
|
||||||
pendingNumAccelerators.to == null
|
pendingNumAccelerators.to == null
|
||||||
) {
|
) {
|
||||||
pendingNumAccelerators = { from: 0, to: maxNumAccelerators };
|
accState = { from: 0, to: maxNumAccelerators };
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function setResources() {
|
||||||
|
if (nodesActive) {
|
||||||
|
pendingNumNodes = {...nodesState};
|
||||||
|
} else {
|
||||||
|
pendingNumNodes = { from: null, to: null };
|
||||||
|
};
|
||||||
|
if (threadActive) {
|
||||||
|
pendingNumHWThreads = {...threadState};
|
||||||
|
} else {
|
||||||
|
pendingNumHWThreads = { from: null, to: null };
|
||||||
|
};
|
||||||
|
if (accActive) {
|
||||||
|
pendingNumAccelerators = {...accState};
|
||||||
|
} else {
|
||||||
|
pendingNumAccelerators = { from: null, to: null };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function resetResources() {
|
||||||
|
pendingNumNodes = { from: null, to: null };
|
||||||
|
pendingNumHWThreads = { from: null, to: null };
|
||||||
|
pendingNumAccelerators = { from: null, to: null };
|
||||||
|
pendingNamedNode = null;
|
||||||
|
pendingNodeMatch = "eq";
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>Select number of utilized Resources</ModalHeader>
|
<ModalHeader>Select number of utilized Resources</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h6>Named Node</h6>
|
<div><b>Named Node</b></div>
|
||||||
<div class="d-flex">
|
<div class="d-flex mb-3">
|
||||||
<Input type="text" class="w-75" bind:value={pendingNamedNode} />
|
<Input type="text" class="w-75" bind:value={pendingNamedNode} />
|
||||||
<div class="mx-1"></div>
|
<div class="mx-1"></div>
|
||||||
<Input type="select" class="w-25" bind:value={pendingNodeMatch}>
|
<Input type="select" class="w-25" bind:value={pendingNodeMatch}>
|
||||||
@@ -164,82 +211,63 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</Input>
|
</Input>
|
||||||
</div>
|
</div>
|
||||||
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
|
|
||||||
<DoubleRangeSlider
|
<div class="mb-3">
|
||||||
on:change={({ detail }) => {
|
<div class="mb-0"><b>Number of Nodes</b></div>
|
||||||
pendingNumNodes = { from: detail[0], to: detail[1] };
|
|
||||||
isNodesModified = true;
|
|
||||||
}}
|
|
||||||
min={minNumNodes}
|
|
||||||
max={maxNumNodes}
|
|
||||||
firstSlider={pendingNumNodes.from}
|
|
||||||
secondSlider={pendingNumNodes.to}
|
|
||||||
inputFieldFrom={pendingNumNodes.from}
|
|
||||||
inputFieldTo={pendingNumNodes.to}
|
|
||||||
/>
|
|
||||||
<h6 style="margin-top: 1rem;">
|
|
||||||
Number of HWThreads (Use for Single-Node Jobs)
|
|
||||||
</h6>
|
|
||||||
<DoubleRangeSlider
|
|
||||||
on:change={({ detail }) => {
|
|
||||||
pendingNumHWThreads = { from: detail[0], to: detail[1] };
|
|
||||||
isHwthreadsModified = true;
|
|
||||||
}}
|
|
||||||
min={minNumHWThreads}
|
|
||||||
max={maxNumHWThreads}
|
|
||||||
firstSlider={pendingNumHWThreads.from}
|
|
||||||
secondSlider={pendingNumHWThreads.to}
|
|
||||||
inputFieldFrom={pendingNumHWThreads.from}
|
|
||||||
inputFieldTo={pendingNumHWThreads.to}
|
|
||||||
/>
|
|
||||||
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
|
|
||||||
<h6 style="margin-top: 1rem;">Number of Accelerators</h6>
|
|
||||||
<DoubleRangeSlider
|
<DoubleRangeSlider
|
||||||
on:change={({ detail }) => {
|
changeRange={(detail) => {
|
||||||
pendingNumAccelerators = { from: detail[0], to: detail[1] };
|
nodesState.from = detail[0];
|
||||||
isAccsModified = true;
|
nodesState.to = detail[1];
|
||||||
}}
|
}}
|
||||||
min={minNumAccelerators}
|
sliderMin={minNumNodes}
|
||||||
max={maxNumAccelerators}
|
sliderMax={maxNumNodes}
|
||||||
firstSlider={pendingNumAccelerators.from}
|
fromPreset={nodesState.from}
|
||||||
secondSlider={pendingNumAccelerators.to}
|
toPreset={nodesState.to}
|
||||||
inputFieldFrom={pendingNumAccelerators.from}
|
|
||||||
inputFieldTo={pendingNumAccelerators.to}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="mb-0"><b>Number of HWThreads</b> (Use for Single-Node Jobs)</div>
|
||||||
|
<DoubleRangeSlider
|
||||||
|
changeRange={(detail) => {
|
||||||
|
threadState.from = detail[0];
|
||||||
|
threadState.to = detail[1];
|
||||||
|
}}
|
||||||
|
sliderMin={1}
|
||||||
|
sliderMax={maxNumHWThreads}
|
||||||
|
fromPreset={threadState.from}
|
||||||
|
toPreset={threadState.to}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
|
||||||
|
<div>
|
||||||
|
<div class="mb-0"><b>Number of Accelerators</b></div>
|
||||||
|
<DoubleRangeSlider
|
||||||
|
changeRange={(detail) => {
|
||||||
|
accState.from = detail[0];
|
||||||
|
accState.to = detail[1];
|
||||||
|
}}
|
||||||
|
sliderMin={0}
|
||||||
|
sliderMax={maxNumAccelerators}
|
||||||
|
fromPreset={accState.from}
|
||||||
|
toPreset={accState.to}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
|
disabled={disableApply}
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
pendingNumNodes = isNodesModified
|
setResources();
|
||||||
? pendingNumNodes
|
setFilter({
|
||||||
: { from: null, to: null };
|
numNodes: pendingNumNodes,
|
||||||
pendingNumHWThreads = isHwthreadsModified
|
numHWThreads: pendingNumHWThreads,
|
||||||
? pendingNumHWThreads
|
numAccelerators: pendingNumAccelerators,
|
||||||
: { from: null, to: null };
|
node: pendingNamedNode,
|
||||||
pendingNumAccelerators = isAccsModified
|
nodeMatch: pendingNodeMatch
|
||||||
? pendingNumAccelerators
|
|
||||||
: { from: null, to: null };
|
|
||||||
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
|
|
||||||
numHWThreads = {
|
|
||||||
from: pendingNumHWThreads.from,
|
|
||||||
to: pendingNumHWThreads.to,
|
|
||||||
};
|
|
||||||
numAccelerators = {
|
|
||||||
from: pendingNumAccelerators.from,
|
|
||||||
to: pendingNumAccelerators.to,
|
|
||||||
};
|
|
||||||
namedNode = pendingNamedNode;
|
|
||||||
nodeMatch = pendingNodeMatch;
|
|
||||||
dispatch("set-filter", {
|
|
||||||
numNodes,
|
|
||||||
numHWThreads,
|
|
||||||
numAccelerators,
|
|
||||||
namedNode,
|
|
||||||
nodeMatch
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -247,36 +275,18 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
pendingNumNodes = { from: null, to: null };
|
resetResources();
|
||||||
pendingNumHWThreads = { from: null, to: null };
|
setFilter({
|
||||||
pendingNumAccelerators = { from: null, to: null };
|
numNodes: pendingNumNodes,
|
||||||
pendingNamedNode = null;
|
numHWThreads: pendingNumHWThreads,
|
||||||
pendingNodeMatch = null;
|
numAccelerators: pendingNumAccelerators,
|
||||||
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to };
|
node: pendingNamedNode,
|
||||||
numHWThreads = {
|
nodeMatch: pendingNodeMatch
|
||||||
from: pendingNumHWThreads.from,
|
|
||||||
to: pendingNumHWThreads.to,
|
|
||||||
};
|
|
||||||
numAccelerators = {
|
|
||||||
from: pendingNumAccelerators.from,
|
|
||||||
to: pendingNumAccelerators.to,
|
|
||||||
};
|
|
||||||
isNodesModified = false;
|
|
||||||
isHwthreadsModified = false;
|
|
||||||
isAccsModified = false;
|
|
||||||
namedNode = pendingNamedNode;
|
|
||||||
nodeMatch = pendingNodeMatch;
|
|
||||||
dispatch("set-filter", {
|
|
||||||
numNodes,
|
|
||||||
numHWThreads,
|
|
||||||
numAccelerators,
|
|
||||||
namedNode,
|
|
||||||
nodeMatch
|
|
||||||
});
|
});
|
||||||
}}>Reset</Button
|
}}>Reset</Button
|
||||||
>
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button onclick={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -12,8 +12,19 @@
|
|||||||
- `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component
|
- `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<script module>
|
||||||
|
export const startTimeSelectOptions = [
|
||||||
|
{ range: "", rangeLabel: "No Selection"},
|
||||||
|
{ range: "last6h", rangeLabel: "Last 6hrs"},
|
||||||
|
{ range: "last24h", rangeLabel: "Last 24hrs"},
|
||||||
|
{ range: "last7d", rangeLabel: "Last 7 days"},
|
||||||
|
{ range: "last30d", rangeLabel: "Last 30 days"}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
/* Note: Ignore VSCode reported 'A component can only have one instance-level <script> element' error */
|
||||||
|
|
||||||
import { parse, format, sub } from "date-fns";
|
import { parse, format, sub } from "date-fns";
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
@@ -26,44 +37,39 @@
|
|||||||
FormGroup,
|
FormGroup,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
export let isModified = false;
|
isOpen = $bindable(false),
|
||||||
export let isOpen = false;
|
presetStartTime = { from: null, to: null, range: "" },
|
||||||
export let from = null;
|
setFilter
|
||||||
export let to = null;
|
} = $props();
|
||||||
export let range = "";
|
|
||||||
export let startTimeSelectOptions;
|
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const now = new Date(Date.now());
|
const now = new Date(Date.now());
|
||||||
const ago = sub(now, { months: 1 });
|
const ago = sub(now, { months: 1 });
|
||||||
const defaultFrom = {
|
const resetFrom = { date: format(ago, "yyyy-MM-dd"), time: format(ago, "HH:mm")};
|
||||||
date: format(ago, "yyyy-MM-dd"),
|
const resetTo = { date: format(now, "yyyy-MM-dd"), time: format(now, "HH:mm")};
|
||||||
time: format(ago, "HH:mm"),
|
|
||||||
};
|
|
||||||
const defaultTo = {
|
|
||||||
date: format(now, "yyyy-MM-dd"),
|
|
||||||
time: format(now, "HH:mm"),
|
|
||||||
};
|
|
||||||
|
|
||||||
$: pendingFrom = (from == null) ? defaultFrom : fromRFC3339(from)
|
/* State Init */
|
||||||
$: pendingTo = (to == null) ? defaultTo : fromRFC3339(to)
|
let pendingStartTime = $state(presetStartTime);
|
||||||
$: pendingRange = range
|
let fromState = $state(fromRFC3339(presetStartTime?.from, resetFrom));
|
||||||
|
let toState = $state(fromRFC3339(presetStartTime?.to, resetTo));
|
||||||
|
|
||||||
$: isModified =
|
/* Derived Init*/
|
||||||
(from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, "59")) &&
|
const rangeSelect = $derived(pendingStartTime?.range ? pendingStartTime.range : "")
|
||||||
(range != pendingRange) &&
|
|
||||||
!(
|
/* Functions */
|
||||||
from == null &&
|
function fromRFC3339(rfc3339, reset) {
|
||||||
pendingFrom.date == "0000-00-00" &&
|
if (rfc3339) {
|
||||||
pendingFrom.time == "00:00"
|
const parsedDate = new Date(rfc3339);
|
||||||
) &&
|
return {
|
||||||
!(
|
date: format(parsedDate, "yyyy-MM-dd"),
|
||||||
to == null &&
|
time: format(parsedDate, "HH:mm"),
|
||||||
pendingTo.date == "0000-00-00" &&
|
}
|
||||||
pendingTo.time == "00:00"
|
} else {
|
||||||
) &&
|
return reset
|
||||||
!( range == "" && pendingRange == "");
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toRFC3339({ date, time }, secs = "00") {
|
function toRFC3339({ date, time }, secs = "00") {
|
||||||
const parsedDate = parse(
|
const parsedDate = parse(
|
||||||
@@ -73,26 +79,18 @@
|
|||||||
);
|
);
|
||||||
return parsedDate.toISOString();
|
return parsedDate.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromRFC3339(rfc3339) {
|
|
||||||
const parsedDate = new Date(rfc3339);
|
|
||||||
return {
|
|
||||||
date: format(parsedDate, "yyyy-MM-dd"),
|
|
||||||
time: format(parsedDate, "HH:mm"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>Select Start Time</ModalHeader>
|
<ModalHeader>Select Start Time</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#if range !== ""}
|
{#if rangeSelect !== ""}
|
||||||
<h4>Current Range</h4>
|
<h4>Current Range</h4>
|
||||||
<Row>
|
<Row>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type ="select" bind:value={pendingRange} >
|
<Input type ="select" bind:value={pendingStartTime.range} >
|
||||||
{#each startTimeSelectOptions as { rangeLabel, range }}
|
{#each startTimeSelectOptions as { rangeLabel, range }}
|
||||||
<option label={rangeLabel} value={range}/>
|
<option label={rangeLabel} value={range}></option>
|
||||||
{/each}
|
{/each}
|
||||||
</Input>
|
</Input>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -101,42 +99,41 @@
|
|||||||
<h4>From</h4>
|
<h4>From</h4>
|
||||||
<Row>
|
<Row>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type="date" bind:value={pendingFrom.date} disabled={pendingRange !== ""}/>
|
<Input type="date" bind:value={fromState.date} disabled={rangeSelect !== ""}/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type="time" bind:value={pendingFrom.time} disabled={pendingRange !== ""}/>
|
<Input type="time" bind:value={fromState.time} disabled={rangeSelect !== ""}/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Row>
|
</Row>
|
||||||
<h4>To</h4>
|
<h4>To</h4>
|
||||||
<Row>
|
<Row>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type="date" bind:value={pendingTo.date} disabled={pendingRange !== ""}/>
|
<Input type="date" bind:value={toState.date} disabled={rangeSelect !== ""}/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup class="col">
|
<FormGroup class="col">
|
||||||
<Input type="time" bind:value={pendingTo.time} disabled={pendingRange !== ""}/>
|
<Input type="time" bind:value={toState.time} disabled={rangeSelect !== ""}/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Row>
|
</Row>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{#if pendingRange !== ""}
|
{#if rangeSelect !== ""}
|
||||||
<Button
|
<Button
|
||||||
color="warning"
|
color="warning"
|
||||||
disabled={pendingRange === ""}
|
disabled={rangeSelect === ""}
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
pendingRange = ""
|
pendingStartTime.range = "";
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset Range
|
Reset Range
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={pendingRange === ""}
|
disabled={rangeSelect === ""}
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
from = null;
|
pendingStartTime.from = null;
|
||||||
to = null;
|
pendingStartTime.to = null;
|
||||||
range = pendingRange;
|
setFilter({ startTime: pendingStartTime });
|
||||||
dispatch("set-filter", { from, to, range });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Close & Apply Range
|
Close & Apply Range
|
||||||
@@ -144,14 +141,14 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={pendingFrom.date == "0000-00-00" ||
|
disabled={fromState.date == "0000-00-00" ||
|
||||||
pendingTo.date == "0000-00-00"}
|
toState.date == "0000-00-00"}
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
from = toRFC3339(pendingFrom);
|
pendingStartTime.from = toRFC3339(fromState);
|
||||||
to = toRFC3339(pendingTo, "59");
|
pendingStartTime.to = toRFC3339(toState, "59");
|
||||||
range = "";
|
pendingStartTime.range = "";
|
||||||
dispatch("set-filter", { from, to, range });
|
setFilter({ startTime: pendingStartTime });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Close & Apply Dates
|
Close & Apply Dates
|
||||||
@@ -159,14 +156,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
from = null;
|
fromState = resetFrom;
|
||||||
to = null;
|
toState = resetTo;
|
||||||
range = "";
|
pendingStartTime.from = null;
|
||||||
dispatch("set-filter", { from, to, range });
|
pendingStartTime.to = null;
|
||||||
|
pendingStartTime.range = "";
|
||||||
|
setFilter({ startTime: pendingStartTime });
|
||||||
}}>Reset</Button
|
}}>Reset</Button
|
||||||
>
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button onclick={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
@component Filter sub-component for selecting job statistics
|
@component Filter sub-component for selecting job statistics
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `isModified Bool?`: Is this filter component modified [Default: false]
|
|
||||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||||
- `stats [Object]?`: The currently selected statistics filter [Default: []]
|
- `stats [Object]?`: The currently selected statistics filter [Default: []]
|
||||||
|
|
||||||
@@ -11,7 +10,6 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
|
||||||
import { getStatsItems } from "../utils.js";
|
import { getStatsItems } from "../utils.js";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -22,75 +20,68 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
|
import DoubleRangeSlider from "../select/DoubleRangeSlider.svelte";
|
||||||
|
|
||||||
const initialized = getContext("initialized"),
|
/* Svelte 5 Props */
|
||||||
dispatch = createEventDispatcher();
|
let {
|
||||||
|
isOpen = $bindable(),
|
||||||
|
presetStats,
|
||||||
|
setFilter
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let isModified = false;
|
/* Derived Init */
|
||||||
export let isOpen = false;
|
const availableStats = $derived(getStatsItems(presetStats));
|
||||||
export let stats = [];
|
|
||||||
|
|
||||||
let statistics = [];
|
|
||||||
|
|
||||||
function loadRanges(isInitialized) {
|
|
||||||
if (!isInitialized) return;
|
|
||||||
statistics = getStatsItems(stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function resetRanges() {
|
function resetRanges() {
|
||||||
for (let st of statistics) {
|
for (let as of availableStats) {
|
||||||
st.enabled = false
|
as.enabled = false
|
||||||
st.from = 0
|
as.from = 0
|
||||||
st.to = st.peak
|
as.to = as.peak
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
$: isModified = !statistics.every((a) => {
|
|
||||||
let b = stats.find((s) => s.field == a.field);
|
|
||||||
if (b == null) return !a.enabled;
|
|
||||||
|
|
||||||
return a.from == b.from && a.to == b.to;
|
|
||||||
});
|
|
||||||
|
|
||||||
$: loadRanges($initialized);
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
<ModalHeader>Filter based on statistics</ModalHeader>
|
<ModalHeader>
|
||||||
|
<span>Filter based on statistics</span>
|
||||||
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#each statistics as stat}
|
{#each availableStats as aStat}
|
||||||
<h4>{stat.text}</h4>
|
<div class="mb-3">
|
||||||
<DoubleRangeSlider
|
<div class="mb-0"><b>{aStat.text}</b></div>
|
||||||
on:change={({ detail }) => (
|
<DoubleRangeSlider
|
||||||
(stat.from = detail[0]), (stat.to = detail[1]), (stat.enabled = true)
|
changeRange={(detail) => {
|
||||||
)}
|
aStat.from = detail[0];
|
||||||
min={0}
|
aStat.to = detail[1];
|
||||||
max={stat.peak}
|
if (aStat.from == 0 && aStat.to == aStat.peak) {
|
||||||
firstSlider={stat.from}
|
aStat.enabled = false;
|
||||||
secondSlider={stat.to}
|
} else {
|
||||||
inputFieldFrom={stat.from}
|
aStat.enabled = true;
|
||||||
inputFieldTo={stat.to}
|
}
|
||||||
/>
|
}}
|
||||||
|
sliderMin={0.0}
|
||||||
|
sliderMax={aStat.peak}
|
||||||
|
fromPreset={aStat.from}
|
||||||
|
toPreset={aStat.to}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
stats = statistics.filter((stat) => stat.enabled);
|
setFilter({ stats: [...availableStats.filter((as) => as.enabled)] });
|
||||||
dispatch("set-filter", { stats });
|
|
||||||
}}>Close & Apply</Button
|
}}>Close & Apply</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
resetRanges();
|
resetRanges();
|
||||||
stats = [];
|
setFilter({stats: []});
|
||||||
dispatch("set-filter", { stats });
|
|
||||||
}}>Reset</Button
|
}}>Reset</Button
|
||||||
>
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button onclick={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher, getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ListGroup,
|
ListGroup,
|
||||||
@@ -26,20 +26,20 @@
|
|||||||
import { fuzzySearchTags } from "../utils.js";
|
import { fuzzySearchTags } from "../utils.js";
|
||||||
import Tag from "../helper/Tag.svelte";
|
import Tag from "../helper/Tag.svelte";
|
||||||
|
|
||||||
const allTags = getContext("tags"),
|
/* Svelte 5 Props */
|
||||||
initialized = getContext("initialized"),
|
let {
|
||||||
dispatch = createEventDispatcher();
|
isOpen = $bindable(false),
|
||||||
|
presetTags = [],
|
||||||
|
setFilter
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let isModified = false;
|
/* Derived */
|
||||||
export let isOpen = false;
|
const allTags = $derived(getContext("tags"))
|
||||||
export let tags = [];
|
const initialized = $derived(getContext("initialized"))
|
||||||
|
|
||||||
let pendingTags = [...tags];
|
/* State Init */
|
||||||
$: isModified =
|
let pendingTags = $state(presetTags);
|
||||||
tags.length != pendingTags.length ||
|
let searchTerm = $state("");
|
||||||
!tags.every((tagId) => pendingTags.includes(tagId));
|
|
||||||
|
|
||||||
let searchTerm = "";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() =>
|
onclick={() =>
|
||||||
(pendingTags = pendingTags.filter((id) => id != tag.id))}
|
(pendingTags = pendingTags.filter((id) => id != tag.id))}
|
||||||
>
|
>
|
||||||
<Icon name="dash-circle" />
|
<Icon name="dash-circle" />
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
color="success"
|
color="success"
|
||||||
on:click={() => (pendingTags = [...pendingTags, tag.id])}
|
onclick={() => (pendingTags = [...pendingTags, tag.id])}
|
||||||
>
|
>
|
||||||
<Icon name="plus-circle" />
|
<Icon name="plus-circle" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -81,21 +81,25 @@
|
|||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
tags = [...pendingTags];
|
setFilter({ tags: [...pendingTags] });
|
||||||
dispatch("set-filter", { tags });
|
|
||||||
}}>Close & Apply</Button
|
}}>Close & Apply</Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="warning"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
|
||||||
tags = [];
|
|
||||||
pendingTags = [];
|
pendingTags = [];
|
||||||
dispatch("set-filter", { tags });
|
}}>Clear Selection</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
onclick={() => {
|
||||||
|
isOpen = false;
|
||||||
|
pendingTags = [];
|
||||||
|
setFilter({ tags: [...pendingTags] });
|
||||||
}}>Reset</Button
|
}}>Reset</Button
|
||||||
>
|
>
|
||||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
<Button onclick={() => (isOpen = false)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -17,11 +17,14 @@
|
|||||||
Icon
|
Icon
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let cJobs;
|
/* Svelte 5 Props */
|
||||||
export let showLinks = false;
|
let {
|
||||||
export let renderCard = false;
|
cJobs,
|
||||||
export let width = "auto";
|
showLinks = false,
|
||||||
export let height = "400px";
|
renderCard = false,
|
||||||
|
width = "auto",
|
||||||
|
height = "400px",
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if renderCard}
|
{#if renderCard}
|
||||||
|
|||||||
@@ -23,79 +23,90 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { findJobFootprintThresholds } from "../utils.js";
|
import { findJobFootprintThresholds } from "../utils.js";
|
||||||
|
|
||||||
export let job;
|
/* Svelte 5 Props */
|
||||||
export let displayTitle = true;
|
let {
|
||||||
export let width = "auto";
|
job,
|
||||||
export let height = "310px";
|
displayTitle = true,
|
||||||
|
width = "auto",
|
||||||
|
height = "310px",
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const footprintData = job?.footprint?.map((jf) => {
|
/* Derived */
|
||||||
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
|
const footprintData = $derived(buildFootprint(job?.footprint));
|
||||||
if (fmc) {
|
|
||||||
// Unit
|
/* Functions */
|
||||||
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
|
function buildFootprint(input) {
|
||||||
|
let result = input?.map((jf) => {
|
||||||
|
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
|
||||||
|
if (fmc) {
|
||||||
|
// Unit
|
||||||
|
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
|
||||||
|
|
||||||
// Threshold / -Differences
|
// Threshold / -Differences
|
||||||
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
|
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
|
||||||
|
|
||||||
// Define basic data -> Value: Use as Provided
|
// Define basic data -> Value: Use as Provided
|
||||||
const fmBase = {
|
const fmBase = {
|
||||||
name: jf.name + ' (' + jf.stat + ')',
|
name: jf.name + ' (' + jf.stat + ')',
|
||||||
avg: jf.value,
|
avg: jf.value,
|
||||||
unit: unit,
|
unit: unit,
|
||||||
max: fmt.peak,
|
max: fmt.peak,
|
||||||
dir: fmc.lowerIsBetter
|
dir: fmc.lowerIsBetter
|
||||||
};
|
|
||||||
|
|
||||||
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
|
|
||||||
return {
|
|
||||||
...fmBase,
|
|
||||||
color: "danger",
|
|
||||||
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
|
||||||
impact: 3
|
|
||||||
};
|
};
|
||||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
|
|
||||||
|
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "danger",
|
||||||
|
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
||||||
|
impact: 3
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "warning",
|
||||||
|
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
||||||
|
impact: 2,
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "success",
|
||||||
|
message: "Footprint value within expected thresholds.",
|
||||||
|
impact: 1,
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "info",
|
||||||
|
message:
|
||||||
|
"Footprint value above expected normal threshold: Check for artifacts recommended.",
|
||||||
|
impact: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "secondary",
|
||||||
|
message:
|
||||||
|
"Footprint value above expected peak threshold: Check for artifacts!",
|
||||||
|
impact: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else { // No matching metric config: display as single value
|
||||||
return {
|
return {
|
||||||
...fmBase,
|
name: jf.name + ' (' + jf.stat + ')',
|
||||||
color: "warning",
|
avg: jf.value,
|
||||||
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
|
||||||
impact: 2,
|
|
||||||
};
|
|
||||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
|
|
||||||
return {
|
|
||||||
...fmBase,
|
|
||||||
color: "success",
|
|
||||||
message: "Footprint value within expected thresholds.",
|
|
||||||
impact: 1,
|
|
||||||
};
|
|
||||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
|
||||||
return {
|
|
||||||
...fmBase,
|
|
||||||
color: "info",
|
|
||||||
message:
|
message:
|
||||||
"Footprint value above expected normal threshold: Check for artifacts recommended.",
|
`No config for metric ${jf.name} found.`,
|
||||||
impact: 0,
|
impact: 4,
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...fmBase,
|
|
||||||
color: "secondary",
|
|
||||||
message:
|
|
||||||
"Footprint value above expected peak threshold: Check for artifacts!",
|
|
||||||
impact: -1,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else { // No matching metric config: display as single value
|
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
|
||||||
return {
|
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
|
||||||
name: jf.name + ' (' + jf.stat + ')',
|
});;
|
||||||
avg: jf.value,
|
|
||||||
message:
|
return result;
|
||||||
`No config for metric ${jf.name} found.`,
|
};
|
||||||
impact: 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
|
|
||||||
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
|
|
||||||
});;
|
|
||||||
|
|
||||||
function evalFootprint(value, thresholds, lowerIsBetter, level) {
|
function evalFootprint(value, thresholds, lowerIsBetter, level) {
|
||||||
// Handle Metrics in which less value is better
|
// Handle Metrics in which less value is better
|
||||||
@@ -176,7 +187,7 @@
|
|||||||
>{fpd.message}</Tooltip
|
>{fpd.message}</Tooltip
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<Row cols={12} class="{(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
|
<Row cols={12} class={(footprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}>
|
||||||
{#if fpd.dir}
|
{#if fpd.dir}
|
||||||
<Col xs="1">
|
<Col xs="1">
|
||||||
<Icon name="caret-left-fill" />
|
<Icon name="caret-left-fill" />
|
||||||
|
|||||||
@@ -8,35 +8,40 @@
|
|||||||
- `refresh`: When fired, the upstream component refreshes its contents
|
- `refresh`: When fired, the upstream component refreshes its contents
|
||||||
-->
|
-->
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
import { Button, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
initially = null,
|
||||||
|
presetClass = "",
|
||||||
|
onRefresh
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let refreshInterval = null;
|
/* State Init */
|
||||||
|
let refreshInterval = $state(initially ? initially * 1000 : null);
|
||||||
|
|
||||||
|
/* Var Init */
|
||||||
let refreshIntervalId = null;
|
let refreshIntervalId = null;
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function refreshIntervalChanged() {
|
function refreshIntervalChanged() {
|
||||||
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
|
if (refreshIntervalId != null) clearInterval(refreshIntervalId);
|
||||||
|
|
||||||
if (refreshInterval == null) return;
|
if (refreshInterval == null) return;
|
||||||
|
refreshIntervalId = setInterval(() => onRefresh(), refreshInterval);
|
||||||
refreshIntervalId = setInterval(() => dispatch("refresh"), refreshInterval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export let initially = null;
|
/* Svelte 5 onMount */
|
||||||
|
$effect(() => {
|
||||||
if (initially != null) {
|
|
||||||
refreshInterval = initially * 1000;
|
|
||||||
refreshIntervalChanged();
|
refreshIntervalChanged();
|
||||||
}
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InputGroup>
|
<InputGroup class={presetClass}>
|
||||||
<Input
|
<Input
|
||||||
type="select"
|
type="select"
|
||||||
title="Periodic refresh interval"
|
title="Periodic refresh interval"
|
||||||
bind:value={refreshInterval}
|
bind:value={refreshInterval}
|
||||||
on:change={refreshIntervalChanged}
|
onchange={refreshIntervalChanged}
|
||||||
>
|
>
|
||||||
<option value={null}>No Interval</option>
|
<option value={null}>No Interval</option>
|
||||||
<option value={30 * 1000}>30 Seconds</option>
|
<option value={30 * 1000}>30 Seconds</option>
|
||||||
@@ -46,7 +51,7 @@
|
|||||||
</Input>
|
</Input>
|
||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
on:click={() => dispatch("refresh")}
|
onclick={() => onRefresh()}
|
||||||
disabled={refreshInterval != null}
|
disabled={refreshInterval != null}
|
||||||
>
|
>
|
||||||
<Icon name="arrow-clockwise" /> Refresh
|
<Icon name="arrow-clockwise" /> Refresh
|
||||||
|
|||||||
@@ -9,21 +9,30 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte'
|
import { getContext } from 'svelte'
|
||||||
const allTags = getContext('tags'),
|
|
||||||
initialized = getContext('initialized')
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
id = null,
|
||||||
|
tag = null,
|
||||||
|
clickable = true
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let id = null
|
/* Derived */
|
||||||
export let tag = null
|
const allTags = $derived(getContext('tags'));
|
||||||
export let clickable = true
|
const initialized = $derived(getContext('initialized'));
|
||||||
|
|
||||||
if (tag != null && id == null)
|
/* Effects */
|
||||||
id = tag.id
|
$effect(() => {
|
||||||
|
if (tag != null && id == null)
|
||||||
|
id = tag.id
|
||||||
|
});
|
||||||
|
|
||||||
$: {
|
$effect(() => {
|
||||||
if ($initialized && tag == null)
|
if ($initialized && tag == null)
|
||||||
tag = allTags.find(tag => tag.id == id)
|
tag = allTags.find(tag => tag.id == id)
|
||||||
}
|
});
|
||||||
|
|
||||||
|
/* Function*/
|
||||||
function getScopeColor(scope) {
|
function getScopeColor(scope) {
|
||||||
switch (scope) {
|
switch (scope) {
|
||||||
case "admin":
|
case "admin":
|
||||||
|
|||||||
@@ -33,26 +33,42 @@
|
|||||||
import { fuzzySearchTags } from "../utils.js";
|
import { fuzzySearchTags } from "../utils.js";
|
||||||
import Tag from "./Tag.svelte";
|
import Tag from "./Tag.svelte";
|
||||||
|
|
||||||
export let job;
|
/* Svelte 5 Props */
|
||||||
export let jobTags = job.tags;
|
let {
|
||||||
export let username;
|
jobTags = $bindable(),
|
||||||
export let authlevel;
|
job,
|
||||||
export let roles;
|
username,
|
||||||
export let renderModal = true;
|
authlevel,
|
||||||
|
roles,
|
||||||
|
renderModal = true,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let allTags = getContext("tags"),
|
/* Const Init */
|
||||||
initialized = getContext("initialized");
|
|
||||||
let newTagType = "",
|
|
||||||
newTagName = "",
|
|
||||||
newTagScope = username;
|
|
||||||
let filterTerm = "";
|
|
||||||
let pendingChange = false;
|
|
||||||
let isOpen = false;
|
|
||||||
const isAdmin = (roles && authlevel == roles.admin);
|
const isAdmin = (roles && authlevel == roles.admin);
|
||||||
const isSupport = (roles && authlevel == roles.support);
|
const isSupport = (roles && authlevel == roles.support);
|
||||||
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let initialized = getContext("initialized")
|
||||||
|
let allTags = getContext("tags")
|
||||||
|
let newTagType = $state("");
|
||||||
|
let newTagName = $state("");
|
||||||
|
let newTagScope = $state(username);
|
||||||
|
let filterTerm = $state("");
|
||||||
|
let pendingChange = $state(false);
|
||||||
|
let isOpen = $state(false);
|
||||||
|
|
||||||
|
/* Derived Init */
|
||||||
|
const allTagsFiltered = $derived(($initialized, jobTags, fuzzySearchTags(filterTerm, allTags))); // $init und JobTags only for triggering react
|
||||||
|
const usedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'used', isAdmin, isSupport));
|
||||||
|
const unusedTagsFiltered = $derived(matchJobTags(jobTags, allTagsFiltered, 'unused', isAdmin, isSupport));
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
$effect(() => {
|
||||||
|
updateNewTag(filterTerm)
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Const Mutations */
|
||||||
const createTagMutation = ({ type, name, scope }) => {
|
const createTagMutation = ({ type, name, scope }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
client: client,
|
client: client,
|
||||||
@@ -104,19 +120,16 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$: allTagsFiltered = ($initialized, fuzzySearchTags(filterTerm, allTags));
|
/* Functions */
|
||||||
$: usedTagsFiltered = matchJobTags(jobTags, allTagsFiltered, 'used', isAdmin, isSupport);
|
function updateNewTag(term) {
|
||||||
$: unusedTagsFiltered = matchJobTags(jobTags, allTagsFiltered, 'unused', isAdmin, isSupport);
|
|
||||||
|
|
||||||
$: {
|
|
||||||
newTagType = "";
|
newTagType = "";
|
||||||
newTagName = "";
|
newTagName = "";
|
||||||
let parts = filterTerm.split(":").map((s) => s.trim());
|
let parts = term.split(":").map((s) => s.trim());
|
||||||
if (parts.length == 2 && parts.every((s) => s.length > 0)) {
|
if (parts.length == 2 && parts.every((s) => s.length > 0)) {
|
||||||
newTagType = parts[0];
|
newTagType = parts[0];
|
||||||
newTagName = parts[1];
|
newTagName = parts[1];
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
function matchJobTags(tags, availableTags, type, isAdmin, isSupport) {
|
function matchJobTags(tags, availableTags, type, isAdmin, isSupport) {
|
||||||
const jobTagIds = tags.map((t) => t.id)
|
const jobTagIds = tags.map((t) => t.id)
|
||||||
@@ -183,7 +196,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if renderModal}
|
{#if renderModal}
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => {isOpen = !isOpen; filterTerm = "";}}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Manage Tags <Icon name="tags"/>
|
Manage Tags <Icon name="tags"/>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
@@ -232,7 +245,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => removeTagFromJob(utag)}
|
onclick={() => removeTagFromJob(utag)}
|
||||||
>
|
>
|
||||||
<Icon name="x" />
|
<Icon name="x" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -282,7 +295,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="success"
|
color="success"
|
||||||
on:click={() => addTagToJob(uutag)}
|
onclick={() => addTagToJob(uutag)}
|
||||||
>
|
>
|
||||||
<Icon name="plus" />
|
<Icon name="plus" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -314,7 +327,7 @@
|
|||||||
outline
|
outline
|
||||||
style="width:100%;"
|
style="width:100%;"
|
||||||
color="success"
|
color="success"
|
||||||
on:click={(e) => (
|
onclick={(e) => (
|
||||||
e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
|
e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -345,11 +358,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={() => (isOpen = false)}>Close</Button>
|
<Button color="primary" onclick={() => {isOpen = false; filterTerm = "";}}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Button outline on:click={() => (isOpen = true)} size="sm" color="primary">
|
<Button outline onclick={() => (isOpen = true)} size="sm" color="primary">
|
||||||
Manage {jobTags?.length ? jobTags.length : ''} Tags
|
Manage {jobTags?.length ? jobTags.length : ''} Tags
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -387,7 +400,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => removeTagFromJob(utag)}
|
onclick={() => removeTagFromJob(utag)}
|
||||||
>
|
>
|
||||||
<Icon name="x" />
|
<Icon name="x" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -396,7 +409,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="danger"
|
color="danger"
|
||||||
on:click={() => removeTagFromJob(utag)}
|
onclick={() => removeTagFromJob(utag)}
|
||||||
>
|
>
|
||||||
<Icon name="x" />
|
<Icon name="x" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -435,7 +448,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="success"
|
color="success"
|
||||||
on:click={() => addTagToJob(uutag)}
|
onclick={() => addTagToJob(uutag)}
|
||||||
>
|
>
|
||||||
<Icon name="plus" />
|
<Icon name="plus" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -444,7 +457,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="success"
|
color="success"
|
||||||
on:click={() => addTagToJob(uutag)}
|
onclick={() => addTagToJob(uutag)}
|
||||||
>
|
>
|
||||||
<Icon name="plus" />
|
<Icon name="plus" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -475,7 +488,7 @@
|
|||||||
outline
|
outline
|
||||||
style="width:100%;"
|
style="width:100%;"
|
||||||
color="success"
|
color="success"
|
||||||
on:click={(e) => (
|
onclick={(e) => (
|
||||||
e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
|
e.preventDefault(), createTag(newTagType, newTagName, newTagScope)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,21 +12,31 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
|
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import { scramble, scrambleNames } from "../utils.js";
|
import { scramble, scrambleNames } from "../utils.js";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
// Note: If page with this component has project preset, keep preset until reset
|
||||||
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
presetProject = "",
|
||||||
|
authlevel = null,
|
||||||
|
roles = null,
|
||||||
|
setFilter
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let presetProject = ""; // If page with this component has project preset, keep preset until reset
|
/* Const Init*/
|
||||||
export let authlevel = null;
|
const throttle = 500;
|
||||||
export let roles = null;
|
|
||||||
let mode = presetProject ? "jobName" : "project";
|
/* Var Init */
|
||||||
let term = "";
|
|
||||||
let user = "";
|
let user = "";
|
||||||
let project = presetProject ? presetProject : "";
|
let project = presetProject ? presetProject : "";
|
||||||
let jobName = "";
|
let jobName = "";
|
||||||
const throttle = 500;
|
let timeoutId = null;
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let mode = $state(presetProject ? "jobName" : "project");
|
||||||
|
let term = $state("");
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function modeChanged() {
|
function modeChanged() {
|
||||||
if (mode == "user") {
|
if (mode == "user") {
|
||||||
project = presetProject ? presetProject : "";
|
project = presetProject ? presetProject : "";
|
||||||
@@ -41,7 +51,6 @@
|
|||||||
termChanged(0);
|
termChanged(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeoutId = null;
|
|
||||||
// Compatibility: Handle "user role" and "no role" identically
|
// Compatibility: Handle "user role" and "no role" identically
|
||||||
function termChanged(sleep = throttle) {
|
function termChanged(sleep = throttle) {
|
||||||
if (roles && authlevel >= roles.manager) {
|
if (roles && authlevel >= roles.manager) {
|
||||||
@@ -52,7 +61,7 @@
|
|||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
dispatch("set-filter", {
|
setFilter({
|
||||||
user,
|
user,
|
||||||
project,
|
project,
|
||||||
jobName
|
jobName
|
||||||
@@ -65,7 +74,7 @@
|
|||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
dispatch("set-filter", {
|
setFilter({
|
||||||
project,
|
project,
|
||||||
jobName
|
jobName
|
||||||
});
|
});
|
||||||
@@ -78,6 +87,8 @@
|
|||||||
term = ""
|
term = ""
|
||||||
presetProject = ""
|
presetProject = ""
|
||||||
project = ""
|
project = ""
|
||||||
|
jobName = ""
|
||||||
|
user = ""
|
||||||
termChanged(0);
|
termChanged(0);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -86,10 +97,10 @@
|
|||||||
<Input
|
<Input
|
||||||
type="select"
|
type="select"
|
||||||
style="max-width: 120px;"
|
style="max-width: 120px;"
|
||||||
class="form-select"
|
class="form-select w-auto"
|
||||||
title="Search Mode"
|
title="Search Mode"
|
||||||
bind:value={mode}
|
bind:value={mode}
|
||||||
on:change={modeChanged}
|
onchange={modeChanged}
|
||||||
>
|
>
|
||||||
{#if !presetProject}
|
{#if !presetProject}
|
||||||
<option value={"project"}>Project</option>
|
<option value={"project"}>Project</option>
|
||||||
@@ -102,12 +113,12 @@
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={term}
|
bind:value={term}
|
||||||
on:change={() => termChanged()}
|
onchange={() => termChanged()}
|
||||||
on:keyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
onkeyup={(event) => termChanged(event.key == "Enter" ? 0 : throttle)}
|
||||||
placeholder={presetProject ? `Find ${mode} in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Find ${mode} ...`}
|
placeholder={presetProject ? `Find in ${scrambleNames ? scramble(presetProject) : presetProject} ...` : `Find ${mode} ...`}
|
||||||
/>
|
/>
|
||||||
{#if presetProject}
|
{#if presetProject}
|
||||||
<Button title="Reset Project" on:click={resetProject}
|
<Button title="Reset Project" onclick={() => resetProject()}
|
||||||
><Icon name="arrow-counterclockwise" /></Button
|
><Icon name="arrow-counterclockwise" /></Button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -12,15 +12,22 @@
|
|||||||
import Tag from "../helper/Tag.svelte";
|
import Tag from "../helper/Tag.svelte";
|
||||||
import TagManagement from "../helper/TagManagement.svelte";
|
import TagManagement from "../helper/TagManagement.svelte";
|
||||||
|
|
||||||
export let job;
|
/* Svelte 5 Props */
|
||||||
export let jobTags = job.tags;
|
let {
|
||||||
export let showTagedit = false;
|
job,
|
||||||
export let username = null;
|
jobTags = job.tags,
|
||||||
export let authlevel= null;
|
showTagedit = false,
|
||||||
export let roles = null;
|
username = null,
|
||||||
export let isSelected = null;
|
authlevel= null,
|
||||||
export let showSelect = false;
|
roles = null,
|
||||||
|
isSelected = null,
|
||||||
|
showSelect = false,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let displayCheck = $state(false);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
function formatDuration(duration) {
|
function formatDuration(duration) {
|
||||||
const hours = Math.floor(duration / 3600);
|
const hours = Math.floor(duration / 3600);
|
||||||
duration -= hours * 3600;
|
duration -= hours * 3600;
|
||||||
@@ -41,9 +48,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayCheck = false;
|
|
||||||
function clipJobId(jid) {
|
function clipJobId(jid) {
|
||||||
displayCheck = true;
|
|
||||||
// Navigator clipboard api needs a secure context (https)
|
// Navigator clipboard api needs a secure context (https)
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
@@ -65,14 +71,11 @@
|
|||||||
textArea.remove();
|
textArea.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTimeout(function () {
|
|
||||||
displayCheck = false;
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2">
|
<p class="mb-2 text-truncate">
|
||||||
<span class="d-flex justify-content-between">
|
<span class="d-flex justify-content-between">
|
||||||
<span class="align-self-center fw-bold mr-2">
|
<span class="align-self-center fw-bold mr-2">
|
||||||
<a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
<a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
||||||
@@ -81,7 +84,7 @@
|
|||||||
<span>
|
<span>
|
||||||
{#if showSelect}
|
{#if showSelect}
|
||||||
<Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2"
|
<Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isSelected = !isSelected
|
isSelected = !isSelected
|
||||||
}}>
|
}}>
|
||||||
{#if isSelected}
|
{#if isSelected}
|
||||||
@@ -98,7 +101,13 @@
|
|||||||
{ 'Add or Remove Job to/from Comparison Selection' }
|
{ 'Add or Remove Job to/from Comparison Selection' }
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" on:click={clipJobId(job.jobId)} >
|
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" onclick={() => {
|
||||||
|
displayCheck = true;
|
||||||
|
clipJobId(job.jobId);
|
||||||
|
setTimeout(function () {
|
||||||
|
displayCheck = false;
|
||||||
|
}, 1000);
|
||||||
|
}}>
|
||||||
{#if displayCheck}
|
{#if displayCheck}
|
||||||
<Icon name="clipboard2-check-fill"/>
|
<Icon name="clipboard2-check-fill"/>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -113,16 +122,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{#if job.metaData?.jobName}
|
{#if job.metaData?.jobName}
|
||||||
{#if job.metaData?.jobName.length <= 25}
|
{#if job.metaData?.jobName.length <= 20}
|
||||||
<div>{job.metaData.jobName}</div>
|
<span>{job.metaData.jobName}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<span
|
||||||
class="truncate"
|
|
||||||
style="cursor:help;"
|
style="cursor:help;"
|
||||||
title={job.metaData.jobName}
|
title={job.metaData.jobName}
|
||||||
>
|
>
|
||||||
{job.metaData.jobName}
|
{job.metaData.jobName}
|
||||||
</div>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if job.arrayJobId}
|
{#if job.arrayJobId}
|
||||||
@@ -207,11 +215,3 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.truncate {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -11,11 +11,51 @@
|
|||||||
- Dispatched once immediately and then each time page or itemsPerPage changes
|
- Dispatched once immediately and then each time page or itemsPerPage changes
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
page = 1,
|
||||||
|
itemsPerPage = 10,
|
||||||
|
totalItems = 0,
|
||||||
|
itemText = "items",
|
||||||
|
pageSizes = [10,25,50],
|
||||||
|
updatePaging
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Derived */
|
||||||
|
const backButtonDisabled = $derived((page === 1));
|
||||||
|
const nextButtonDisabled = $derived((page >= (totalItems / itemsPerPage)));
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function pageUp ( event ) {
|
||||||
|
event.preventDefault();
|
||||||
|
page += 1;
|
||||||
|
updatePaging({ itemsPerPage, page });
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageBack ( event ) {
|
||||||
|
event.preventDefault();
|
||||||
|
page -= 1;
|
||||||
|
updatePaging({ itemsPerPage, page });
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageReset ( event ) {
|
||||||
|
event.preventDefault();
|
||||||
|
page = 1;
|
||||||
|
updatePaging({ itemsPerPage, page });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItems ( event ) {
|
||||||
|
event.preventDefault();
|
||||||
|
updatePaging({ itemsPerPage, page });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="cc-pagination" >
|
<div class="cc-pagination" >
|
||||||
<div class="cc-pagination-left">
|
<div class="cc-pagination-left">
|
||||||
<label for="cc-pagination-select">{ itemText } per page:</label>
|
<label for="cc-pagination-select">{ itemText } per page:</label>
|
||||||
<div class="cc-pagination-select-wrapper">
|
<div class="cc-pagination-select-wrapper">
|
||||||
<select on:blur|preventDefault={reset} bind:value={itemsPerPage} id="cc-pagination-select" class="cc-pagination-select">
|
<select onblur={(e) => pageReset(e)} onchange={(e) => updateItems(e)} bind:value={itemsPerPage} id="cc-pagination-select" class="cc-pagination-select">
|
||||||
{#each pageSizes as size}
|
{#each pageSizes as size}
|
||||||
<option value="{size}">{size}</option>
|
<option value="{size}">{size}</option>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -23,54 +63,23 @@
|
|||||||
<span class="focus"></span>
|
<span class="focus"></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="cc-pagination-text">
|
<span class="cc-pagination-text">
|
||||||
{ (page - 1) * itemsPerPage } - { Math.min((page - 1) * itemsPerPage + itemsPerPage, totalItems) } of { totalItems } { itemText }
|
{ ((page - 1) * itemsPerPage) + 1 } - { Math.min((page - 1) * itemsPerPage + itemsPerPage, totalItems) } of { totalItems } { itemText }
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cc-pagination-right">
|
<div class="cc-pagination-right">
|
||||||
{#if !backButtonDisabled}
|
{#if !backButtonDisabled}
|
||||||
<button class="reset nav" type="button"
|
<button aria-label="page-reset" class="reset nav" type="button"
|
||||||
on:click|preventDefault="{reset}"></button>
|
onclick={(e) => pageReset(e)}></button>
|
||||||
<button class="left nav" type="button"
|
<button aria-label="page-back" class="left nav" type="button"
|
||||||
on:click|preventDefault="{() => { page -= 1; }}"></button>
|
onclick={(e) => pageBack(e)}></button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !nextButtonDisabled}
|
{#if !nextButtonDisabled}
|
||||||
<button class="right nav" type="button"
|
<button aria-label="page-up" class="right nav" type="button"
|
||||||
on:click|preventDefault="{() => { page += 1; }}"></button>
|
onclick={(e) => pageUp(e)}></button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
export let page = 1;
|
|
||||||
export let itemsPerPage = 10;
|
|
||||||
export let totalItems = 0;
|
|
||||||
export let itemText = "items";
|
|
||||||
export let pageSizes = [10,25,50];
|
|
||||||
|
|
||||||
let backButtonDisabled, nextButtonDisabled;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (typeof page !== "number") {
|
|
||||||
page = Number(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof itemsPerPage !== "number") {
|
|
||||||
itemsPerPage = Number(itemsPerPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch("update-paging", { itemsPerPage, page });
|
|
||||||
}
|
|
||||||
$: backButtonDisabled = (page === 1);
|
|
||||||
$: nextButtonDisabled = (page >= (totalItems / itemsPerPage));
|
|
||||||
|
|
||||||
function reset ( event ) {
|
|
||||||
page = 1;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@@ -18,86 +18,30 @@
|
|||||||
import { getContext, onMount, onDestroy } from "svelte";
|
import { getContext, onMount, onDestroy } from "svelte";
|
||||||
import { Card } from "@sveltestrap/sveltestrap";
|
import { Card } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let metric = "";
|
|
||||||
export let width = 0;
|
|
||||||
export let height = 300;
|
|
||||||
export let data = null;
|
|
||||||
export let xlabel = "";
|
|
||||||
export let xticks = [];
|
|
||||||
export let xinfo = [];
|
|
||||||
export let ylabel = "";
|
|
||||||
export let yunit = "";
|
|
||||||
export let title = "";
|
|
||||||
export let forResources = false;
|
|
||||||
export let plotSync;
|
|
||||||
|
|
||||||
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
|
// NOTE: Metric Thresholds non-required, Cluster Mixing Allowed
|
||||||
|
|
||||||
|
/* Svelte 5 Props */
|
||||||
|
let {
|
||||||
|
metric = "",
|
||||||
|
width = 0,
|
||||||
|
height = 300,
|
||||||
|
data = null,
|
||||||
|
xlabel = "",
|
||||||
|
xticks = [],
|
||||||
|
xinfo = [],
|
||||||
|
ylabel = "",
|
||||||
|
yunit = "",
|
||||||
|
title = "",
|
||||||
|
forResources = false,
|
||||||
|
plotSync,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const clusterCockpitConfig = getContext("cc-config");
|
const clusterCockpitConfig = getContext("cc-config");
|
||||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
const lineWidth = clusterCockpitConfig.plot_general_lineWidth / window.devicePixelRatio;
|
||||||
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
const cbmode = clusterCockpitConfig?.plot_general_colorblindMode || false;
|
||||||
|
|
||||||
// UPLOT PLUGIN // converts the legend into a simple tooltip
|
// UPLOT SERIES INIT //
|
||||||
function legendAsTooltipPlugin({
|
|
||||||
className,
|
|
||||||
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
|
||||||
} = {}) {
|
|
||||||
let legendEl;
|
|
||||||
|
|
||||||
function init(u, opts) {
|
|
||||||
legendEl = u.root.querySelector(".u-legend");
|
|
||||||
|
|
||||||
legendEl.classList.remove("u-inline");
|
|
||||||
className && legendEl.classList.add(className);
|
|
||||||
|
|
||||||
uPlot.assign(legendEl.style, {
|
|
||||||
minWidth: "100px",
|
|
||||||
textAlign: "left",
|
|
||||||
pointerEvents: "none",
|
|
||||||
display: "none",
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
zIndex: 100,
|
|
||||||
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
|
||||||
...style,
|
|
||||||
});
|
|
||||||
|
|
||||||
// hide series color markers:
|
|
||||||
const idents = legendEl.querySelectorAll(".u-marker");
|
|
||||||
for (let i = 0; i < idents.length; i++)
|
|
||||||
idents[i].style.display = "none";
|
|
||||||
|
|
||||||
const overEl = u.over;
|
|
||||||
overEl.style.overflow = "visible";
|
|
||||||
|
|
||||||
// move legend into plot bounds
|
|
||||||
overEl.appendChild(legendEl);
|
|
||||||
|
|
||||||
// show/hide tooltip on enter/exit
|
|
||||||
overEl.addEventListener("mouseenter", () => {
|
|
||||||
legendEl.style.display = null;
|
|
||||||
});
|
|
||||||
overEl.addEventListener("mouseleave", () => {
|
|
||||||
legendEl.style.display = "none";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(u) {
|
|
||||||
const { left, top } = u.cursor;
|
|
||||||
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
|
|
||||||
legendEl.style.transform =
|
|
||||||
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hooks: {
|
|
||||||
init: init,
|
|
||||||
setCursor: update,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const plotSeries = [
|
const plotSeries = [
|
||||||
{
|
{
|
||||||
label: "JobID",
|
label: "JobID",
|
||||||
@@ -122,6 +66,7 @@
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// UPLOT SCALES INIT //
|
||||||
if (forResources) {
|
if (forResources) {
|
||||||
const resSeries = [
|
const resSeries = [
|
||||||
{
|
{
|
||||||
@@ -177,11 +122,13 @@
|
|||||||
plotSeries.push(...statsSeries)
|
plotSeries.push(...statsSeries)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// UPLOT BAND COLORS //
|
||||||
const plotBands = [
|
const plotBands = [
|
||||||
{ series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
|
{ series: [5, 4], fill: cbmode ? "rgba(0,0,255,0.1)" : "rgba(0,255,0,0.1)" },
|
||||||
{ series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
|
{ series: [4, 3], fill: cbmode ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// UPLOT OPTIONS //
|
||||||
const opts = {
|
const opts = {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -259,11 +206,83 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// RENDER HANDLING
|
/* Var Init */
|
||||||
let plotWrapper = null;
|
|
||||||
let uplot = null;
|
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
|
let uplot = null;
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let plotWrapper = $state(null);
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
$effect(() => {
|
||||||
|
if (plotWrapper) {
|
||||||
|
onSizeChange(width, height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
// UPLOT PLUGIN // converts the legend into a simple tooltip
|
||||||
|
function legendAsTooltipPlugin({
|
||||||
|
className,
|
||||||
|
style = { backgroundColor: "rgba(255, 249, 196, 0.92)", color: "black" },
|
||||||
|
} = {}) {
|
||||||
|
let legendEl;
|
||||||
|
|
||||||
|
function init(u, opts) {
|
||||||
|
legendEl = u.root.querySelector(".u-legend");
|
||||||
|
|
||||||
|
legendEl.classList.remove("u-inline");
|
||||||
|
className && legendEl.classList.add(className);
|
||||||
|
|
||||||
|
uPlot.assign(legendEl.style, {
|
||||||
|
minWidth: "100px",
|
||||||
|
textAlign: "left",
|
||||||
|
pointerEvents: "none",
|
||||||
|
display: "none",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
|
||||||
|
...style,
|
||||||
|
});
|
||||||
|
|
||||||
|
// hide series color markers:
|
||||||
|
const idents = legendEl.querySelectorAll(".u-marker");
|
||||||
|
for (let i = 0; i < idents.length; i++)
|
||||||
|
idents[i].style.display = "none";
|
||||||
|
|
||||||
|
const overEl = u.over;
|
||||||
|
overEl.style.overflow = "visible";
|
||||||
|
|
||||||
|
// move legend into plot bounds
|
||||||
|
overEl.appendChild(legendEl);
|
||||||
|
|
||||||
|
// show/hide tooltip on enter/exit
|
||||||
|
overEl.addEventListener("mouseenter", () => {
|
||||||
|
legendEl.style.display = null;
|
||||||
|
});
|
||||||
|
overEl.addEventListener("mouseleave", () => {
|
||||||
|
legendEl.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(u) {
|
||||||
|
const { left, top } = u.cursor;
|
||||||
|
const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0;
|
||||||
|
legendEl.style.transform =
|
||||||
|
"translate(" + (left - width - 15) + "px, " + (top + 15) + "px)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hooks: {
|
||||||
|
init: init,
|
||||||
|
setCursor: update,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// RENDER HANDLING
|
||||||
function render(ren_width, ren_height) {
|
function render(ren_width, ren_height) {
|
||||||
if (!uplot) {
|
if (!uplot) {
|
||||||
opts.width = ren_width;
|
opts.width = ren_width;
|
||||||
@@ -284,29 +303,25 @@
|
|||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On Mount */
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (plotWrapper) {
|
if (plotWrapper) {
|
||||||
render(width, height);
|
render(width, height);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* On Destroy */
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (timeoutId != null) clearTimeout(timeoutId);
|
if (timeoutId != null) clearTimeout(timeoutId);
|
||||||
if (uplot) uplot.destroy();
|
if (uplot) uplot.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// This updates plot on all size changes if wrapper (== data) exists
|
|
||||||
$: if (plotWrapper) {
|
|
||||||
onSizeChange(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Define $width Wrapper and NoData Card -->
|
<!-- Define $width Wrapper and NoData Card -->
|
||||||
{#if data && data[0].length > 0}
|
{#if data && data[0].length > 0}
|
||||||
<div bind:this={plotWrapper} bind:clientWidth={width}
|
<div bind:this={plotWrapper} bind:clientWidth={width}
|
||||||
style="background-color: rgba(255, 255, 255, 1.0);" class="rounded"
|
style="background-color: rgba(255, 255, 255, 1.0);" class="rounded"
|
||||||
/>
|
></div>
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning" class="mx-4 my-2"
|
<Card body color="warning" class="mx-4 my-2"
|
||||||
>Cannot render plot: No series data returned for <code>{metric?metric:'job resources'}</code></Card
|
>Cannot render plot: No series data returned for <code>{metric?metric:'job resources'}</code></Card
|
||||||
|
|||||||
@@ -244,7 +244,7 @@
|
|||||||
<!-- Define Wrapper and NoData Card within $width -->
|
<!-- Define Wrapper and NoData Card within $width -->
|
||||||
<div bind:clientWidth={width}>
|
<div bind:clientWidth={width}>
|
||||||
{#if data.length > 0}
|
{#if data.length > 0}
|
||||||
<div bind:this={plotWrapper} />
|
<div bind:this={plotWrapper}></div>
|
||||||
{:else}
|
{:else}
|
||||||
<Card class="mx-4" body color="warning"
|
<Card class="mx-4" body color="warning"
|
||||||
>Cannot render histogram: No data!</Card
|
>Cannot render histogram: No data!</Card
|
||||||
|
|||||||
@@ -591,7 +591,7 @@
|
|||||||
{#if series[0]?.data && series[0].data.length > 0}
|
{#if series[0]?.data && series[0].data.length > 0}
|
||||||
<div bind:this={plotWrapper} bind:clientWidth={width}
|
<div bind:this={plotWrapper} bind:clientWidth={width}
|
||||||
style="background-color: {backgroundColor()};" class={forNode ? 'py-2 rounded' : 'rounded'}
|
style="background-color: {backgroundColor()};" class={forNode ? 'py-2 rounded' : 'rounded'}
|
||||||
/>
|
></div>
|
||||||
{:else}
|
{:else}
|
||||||
<Card body color="warning" class="mx-4"
|
<Card body color="warning" class="mx-4"
|
||||||
>Cannot render plot: No series data returned for <code>{metric}</code></Card
|
>Cannot render plot: No series data returned for <code>{metric}</code></Card
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
@component Pie Plot based on uPlot Pie
|
@component Pie Plot based on chart.js Pie
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `size Number`: X and Y size of the plot, for square shape
|
- `size Number`: X and Y size of the plot, for square shape
|
||||||
@@ -31,33 +31,17 @@
|
|||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
import { Pie } from 'svelte-chartjs';
|
import Chart from 'chart.js/auto'
|
||||||
import {
|
import { onMount } from 'svelte';
|
||||||
Chart as ChartJS,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
Filler,
|
|
||||||
ArcElement,
|
|
||||||
CategoryScale
|
|
||||||
} from 'chart.js';
|
|
||||||
|
|
||||||
ChartJS.register(
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
Filler,
|
|
||||||
ArcElement,
|
|
||||||
CategoryScale
|
|
||||||
);
|
|
||||||
|
|
||||||
|
export let canvasId = "pie-default"
|
||||||
export let size
|
export let size
|
||||||
export let sliceLabel
|
export let sliceLabel
|
||||||
export let quantities
|
export let quantities
|
||||||
export let entities
|
export let entities
|
||||||
export let displayLegend = false
|
export let displayLegend = false
|
||||||
|
|
||||||
$: data = {
|
const data = {
|
||||||
labels: entities,
|
labels: entities,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
@@ -79,10 +63,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
new Chart(
|
||||||
|
document.getElementById(canvasId),
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
data: data,
|
||||||
|
options: options
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- <div style="width: 500px;"><canvas id="dimensions"></canvas></div><br/> -->
|
||||||
<div class="chart-container" style="--container-width: {size}; --container-height: {size}">
|
<div class="chart-container" style="--container-width: {size}; --container-height: {size}">
|
||||||
<Pie {data} {options}/>
|
<canvas id={canvasId}></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
@component Polar Plot based on chartJS Radar
|
@component Polar Plot based on chart.js Radar
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ]
|
- `polarMetrics [Object]?`: Metric names and scaled peak values for rendering polar plot [Default: [] ]
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Radar } from 'svelte-chartjs';
|
import { getContext, onMount } from 'svelte'
|
||||||
|
import Chart from 'chart.js/auto'
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
Title,
|
Title,
|
||||||
@@ -30,8 +31,10 @@
|
|||||||
LineElement
|
LineElement
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export let polarMetrics = [];
|
export let polarMetrics = [];
|
||||||
export let polarData = [];
|
export let polarData = [];
|
||||||
|
export let canvasId = "polar-default";
|
||||||
export let height = 350;
|
export let height = 350;
|
||||||
|
|
||||||
const labels = polarMetrics
|
const labels = polarMetrics
|
||||||
@@ -113,10 +116,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
new Chart(
|
||||||
|
document.getElementById(canvasId),
|
||||||
|
{
|
||||||
|
type: 'radar',
|
||||||
|
data: data,
|
||||||
|
options: options,
|
||||||
|
height: height
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- <div style="width: 500px;"><canvas id="dimensions"></canvas></div><br/> -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<Radar {data} {options} {height}/>
|
<canvas id={canvasId}></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -363,7 +363,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if data != null}
|
{#if data != null}
|
||||||
<div bind:this={plotWrapper} class="p-2"/>
|
<div bind:this={plotWrapper} class="p-2"></div>
|
||||||
{:else}
|
{:else}
|
||||||
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
|
<Card class="mx-4" body color="warning">Cannot render roofline: No data!</Card
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Copyright (c) 2021 Michael Keller
|
Copyright (c) 2021 Michael Keller
|
||||||
Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider)
|
Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider)
|
||||||
Changes: remove dependency, text inputs, configurable value ranges, on:change event
|
Changes: remove dependency, text inputs, configurable value ranges, on:change event
|
||||||
|
Changes #2: Rewritten for Svelte 5, removed bodyHandler
|
||||||
-->
|
-->
|
||||||
<!--
|
<!--
|
||||||
@component Selector component to display range selections via min and max double-sliders
|
@component Selector component to display range selections via min and max double-sliders
|
||||||
@@ -9,82 +10,80 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
|
|||||||
Properties:
|
Properties:
|
||||||
- min: Number
|
- min: Number
|
||||||
- max: Number
|
- max: Number
|
||||||
- firstSlider: Number (Starting position of slider #1)
|
- sliderHandleFrom: Number (Starting position of slider #1)
|
||||||
- secondSlider: Number (Starting position of slider #2)
|
- sliderHandleTo: Number (Starting position of slider #2)
|
||||||
|
|
||||||
Events:
|
Events:
|
||||||
- `change`: [Number, Number] (Positions of the two sliders)
|
- `change`: [Number, Number] (Positions of the two sliders)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
let {
|
||||||
|
sliderMin,
|
||||||
|
sliderMax,
|
||||||
|
fromPreset = 1,
|
||||||
|
toPreset = 100,
|
||||||
|
changeRange
|
||||||
|
} = $props();
|
||||||
|
|
||||||
export let min;
|
let pendingValues = $state([fromPreset, toPreset]);
|
||||||
export let max;
|
let sliderFrom = $state(Math.max(((fromPreset == null ? sliderMin : fromPreset) - sliderMin) / (sliderMax - sliderMin), 0.));
|
||||||
export let firstSlider;
|
let sliderTo = $state(Math.min(((toPreset == null ? sliderMin : toPreset) - sliderMin) / (sliderMax - sliderMin), 1.));
|
||||||
export let secondSlider;
|
let inputFieldFrom = $state(fromPreset.toString());
|
||||||
export let inputFieldFrom = 0;
|
let inputFieldTo = $state(toPreset.toString());
|
||||||
export let inputFieldTo = 0;
|
let leftHandle = $state();
|
||||||
|
let sliderMain = $state();
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
let values;
|
|
||||||
let start, end; /* Positions of sliders from 0 to 1 */
|
|
||||||
$: values = [firstSlider, secondSlider]; /* Avoid feedback loop */
|
|
||||||
$: start = Math.max(((firstSlider == null ? min : firstSlider) - min) / (max - min), 0);
|
|
||||||
$: end = Math.min(((secondSlider == null ? min : secondSlider) - min) / (max - min), 1);
|
|
||||||
|
|
||||||
let leftHandle;
|
|
||||||
let body;
|
|
||||||
let slider;
|
|
||||||
|
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
function queueChangeEvent() {
|
function queueChangeEvent() {
|
||||||
if (timeoutId !== null) {
|
if (timeoutId !== null) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
timeoutId = null;
|
timeoutId = null
|
||||||
|
changeRange(pendingValues);
|
||||||
// Show selection but avoid feedback loop
|
}, 100);
|
||||||
if (values[0] != null && inputFieldFrom != values[0].toString())
|
|
||||||
inputFieldFrom = values[0].toString();
|
|
||||||
if (values[1] != null && inputFieldTo != values[1].toString())
|
|
||||||
inputFieldTo = values[1].toString();
|
|
||||||
|
|
||||||
dispatch('change', values);
|
|
||||||
}, 250);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function updateStates(newValue, newPosition, target) {
|
||||||
values = [
|
if (target === 'from') {
|
||||||
Math.floor(min + start * (max - min)),
|
pendingValues[0] = isNaN(newValue) ? null : newValue;
|
||||||
Math.floor(min + end * (max - min))
|
inputFieldFrom = isNaN(newValue) ? null : newValue.toString();
|
||||||
];
|
sliderFrom = newPosition;
|
||||||
queueChangeEvent();
|
} else if (target === 'to') {
|
||||||
}
|
pendingValues[1] = isNaN(newValue) ? null : newValue;
|
||||||
|
inputFieldTo = isNaN(newValue) ? null : newValue.toString();
|
||||||
function inputChanged(idx, event) {
|
sliderTo = newPosition;
|
||||||
let val = Number.parseInt(event.target.value);
|
|
||||||
if (Number.isNaN(val) || val < min) {
|
|
||||||
event.target.classList.add('bad');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
values[idx] = val;
|
|
||||||
event.target.classList.remove('bad');
|
|
||||||
if (idx == 0)
|
|
||||||
start = clamp((val - min) / (max - min), 0., 1.);
|
|
||||||
else
|
|
||||||
end = clamp((val - min) / (max - min), 0., 1.);
|
|
||||||
|
|
||||||
queueChangeEvent();
|
queueChangeEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(x, min, max) {
|
function rangeChanged (evt, target) {
|
||||||
return x < min
|
evt.preventDefault()
|
||||||
? min
|
evt.stopPropagation()
|
||||||
: (x < max ? x : max);
|
const { left, right } = sliderMain.getBoundingClientRect();
|
||||||
|
const parentWidth = right - left;
|
||||||
|
const newP = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
|
||||||
|
const newV = Math.floor(sliderMin + newP * (sliderMax - sliderMin));
|
||||||
|
updateStates(newV, newP, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputChanged(evt, target) {
|
||||||
|
evt.preventDefault()
|
||||||
|
evt.stopPropagation()
|
||||||
|
const newV = Number.parseInt(evt.target.value);
|
||||||
|
const newP = clamp((newV - sliderMin) / (sliderMax - sliderMin), 0., 1.)
|
||||||
|
updateStates(newV, newP, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(x, testMin, testMax) {
|
||||||
|
return x < testMin
|
||||||
|
? testMin
|
||||||
|
: (x > testMax
|
||||||
|
? testMax
|
||||||
|
: x
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function draggable(node) {
|
function draggable(node) {
|
||||||
@@ -151,84 +150,38 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setHandlePosition (which) {
|
|
||||||
return function (evt) {
|
|
||||||
const { left, right } = slider.getBoundingClientRect();
|
|
||||||
const parentWidth = right - left;
|
|
||||||
|
|
||||||
const p = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
|
|
||||||
|
|
||||||
if (which === 'start') {
|
|
||||||
start = p;
|
|
||||||
end = Math.max(end, p);
|
|
||||||
} else {
|
|
||||||
start = Math.min(p, start);
|
|
||||||
end = p;
|
|
||||||
}
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setHandlesFromBody (evt) {
|
|
||||||
const { width } = body.getBoundingClientRect();
|
|
||||||
const { left, right } = slider.getBoundingClientRect();
|
|
||||||
|
|
||||||
const parentWidth = right - left;
|
|
||||||
|
|
||||||
const leftHandleLeft = leftHandle.getBoundingClientRect().left;
|
|
||||||
|
|
||||||
const pxStart = clamp((leftHandleLeft + evt.detail.dx) - left, 0, parentWidth - width);
|
|
||||||
const pxEnd = clamp(pxStart + width, width, parentWidth);
|
|
||||||
|
|
||||||
const pStart = pxStart / parentWidth;
|
|
||||||
const pEnd = pxEnd / parentWidth;
|
|
||||||
|
|
||||||
start = pStart;
|
|
||||||
end = pEnd;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="double-range-container">
|
<div class="double-range-container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<input class="form-control" type="text" placeholder="from..." bind:value={inputFieldFrom}
|
<input class="form-control" type="text" placeholder="from..." value={inputFieldFrom}
|
||||||
on:input={(e) => inputChanged(0, e)} />
|
oninput={(e) => inputChanged(e, 'from')} />
|
||||||
|
|
||||||
<span>Full Range: <b> {min} </b> - <b> {max} </b></span>
|
<span>Full Range: <b> {sliderMin} </b> - <b> {sliderMax} </b></span>
|
||||||
|
|
||||||
<input class="form-control" type="text" placeholder="to..." bind:value={inputFieldTo}
|
<input class="form-control" type="text" placeholder="to..." value={inputFieldTo}
|
||||||
on:input={(e) => inputChanged(1, e)} />
|
oninput={(e) => inputChanged(e, 'to')} />
|
||||||
</div>
|
</div>
|
||||||
<div class="slider" bind:this={slider}>
|
|
||||||
|
<div id="slider-active" class="slider" bind:this={sliderMain}>
|
||||||
<div
|
<div
|
||||||
class="body"
|
class="slider-body"
|
||||||
bind:this={body}
|
style="left: {100 * sliderFrom}%;right: {100 * (1 - sliderTo)}%;"
|
||||||
use:draggable
|
|
||||||
on:dragmove|preventDefault|stopPropagation="{setHandlesFromBody}"
|
|
||||||
style="
|
|
||||||
left: {100 * start}%;
|
|
||||||
right: {100 * (1 - end)}%;
|
|
||||||
"
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="handle"
|
class="slider-handle"
|
||||||
bind:this={leftHandle}
|
bind:this={leftHandle}
|
||||||
data-which="start"
|
data-which="from"
|
||||||
use:draggable
|
use:draggable
|
||||||
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('start')}"
|
ondragmove={(e) => rangeChanged(e, 'from')}
|
||||||
style="
|
style="left: {100 * sliderFrom}%"
|
||||||
left: {100 * start}%
|
|
||||||
"
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="handle"
|
class="slider-handle"
|
||||||
data-which="end"
|
data-which="to"
|
||||||
use:draggable
|
use:draggable
|
||||||
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('end')}"
|
ondragmove={(e) => rangeChanged(e, 'to')}
|
||||||
style="
|
style="left: {100 * sliderTo}%"
|
||||||
left: {100 * end}%
|
|
||||||
"
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,7 +191,8 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: -5px;
|
align-items: flex-end;
|
||||||
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.header :nth-child(2) {
|
.header :nth-child(2) {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
@@ -249,17 +203,13 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.double-range-container .header input[type="text"].bad) {
|
|
||||||
color: #ff5c33;
|
|
||||||
border-color: #ff5c33;
|
|
||||||
}
|
|
||||||
|
|
||||||
.double-range-container {
|
.double-range-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
white-space: nowrap
|
white-space: nowrap;
|
||||||
|
margin-top: -4px;
|
||||||
}
|
}
|
||||||
.slider {
|
.slider {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -271,13 +221,13 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
|
|||||||
box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c;
|
box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.handle {
|
.slider-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
.handle:after {
|
.slider-handle:after {
|
||||||
content: ' ';
|
content: ' ';
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -291,11 +241,11 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
|
|||||||
/* .handle[data-which="end"]:after{
|
/* .handle[data-which="end"]:after{
|
||||||
transform: translate(-100%, -50%);
|
transform: translate(-100%, -50%);
|
||||||
} */
|
} */
|
||||||
.handle:active:after {
|
.slider-handle:active:after {
|
||||||
background-color: #ddd;
|
background-color: #ddd;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
}
|
}
|
||||||
.body {
|
.slider-body {
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #34a1ff;
|
background-color: #34a1ff;
|
||||||
|
|||||||
@@ -20,16 +20,24 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
|
|
||||||
export let cluster;
|
/* Svelte 5 Props */
|
||||||
export let selectedHistograms;
|
let {
|
||||||
export let isOpen;
|
cluster,
|
||||||
|
isOpen = $bindable(),
|
||||||
|
presetSelectedHistograms,
|
||||||
|
applyChange
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const initialized = getContext("initialized");
|
|
||||||
|
|
||||||
function loadHistoMetrics(isInitialized, thisCluster) {
|
/* Derived */
|
||||||
if (!isInitialized) return [];
|
let selectedHistograms = $derived(presetSelectedHistograms); // Non-Const Derived: Is settable
|
||||||
|
const availableMetrics = $derived(loadHistoMetrics(cluster));
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function loadHistoMetrics(thisCluster) {
|
||||||
|
// isInit Check Removed: Parent Component has finished Init-Query: Globalmetrics available here.
|
||||||
if (!thisCluster) {
|
if (!thisCluster) {
|
||||||
return getContext("globalMetrics")
|
return getContext("globalMetrics")
|
||||||
.filter((gm) => gm?.footprint)
|
.filter((gm) => gm?.footprint)
|
||||||
@@ -42,18 +50,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
|
||||||
return mutationStore({
|
|
||||||
client: client,
|
|
||||||
query: gql`
|
|
||||||
mutation ($name: String!, $value: String!) {
|
|
||||||
updateConfiguration(name: $name, value: $value)
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: { name, value },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateConfiguration(data) {
|
function updateConfiguration(data) {
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -67,6 +63,7 @@
|
|||||||
|
|
||||||
function closeAndApply() {
|
function closeAndApply() {
|
||||||
isOpen = !isOpen;
|
isOpen = !isOpen;
|
||||||
|
applyChange(selectedHistograms)
|
||||||
updateConfiguration({
|
updateConfiguration({
|
||||||
name: cluster
|
name: cluster
|
||||||
? `user_view_histogramMetrics:${cluster}`
|
? `user_view_histogramMetrics:${cluster}`
|
||||||
@@ -75,8 +72,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: availableMetrics = loadHistoMetrics($initialized, cluster);
|
/* Mutation */
|
||||||
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
|
return mutationStore({
|
||||||
|
client: client,
|
||||||
|
query: gql`
|
||||||
|
mutation ($name: String!, $value: String!) {
|
||||||
|
updateConfiguration(name: $name, value: $value)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { name, value },
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
<Modal {isOpen} toggle={() => (isOpen = !isOpen)}>
|
||||||
@@ -92,7 +99,7 @@
|
|||||||
</ListGroup>
|
</ListGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
|
<Button color="primary" on:click={() => closeAndApply()}>Close & Apply</Button>
|
||||||
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button>
|
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Close</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext, createEventDispatcher } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
@@ -23,57 +23,23 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
|
|
||||||
export let metrics;
|
/* Svelte 5 Props */
|
||||||
export let isOpen;
|
let {
|
||||||
export let configName;
|
isOpen = $bindable(false),
|
||||||
export let allMetrics = null;
|
showFootprint = $bindable(false),
|
||||||
export let cluster = null;
|
totalMetrics = $bindable(0),
|
||||||
export let subCluster = null;
|
presetMetrics = [],
|
||||||
export let showFootprint = false;
|
cluster = null,
|
||||||
export let footprintSelect = false;
|
subCluster = null,
|
||||||
|
footprintSelect = false,
|
||||||
const onInit = getContext("on-init")
|
preInitialized = false, // Job View is Pre-Init'd: $initialized "alone" store returns false
|
||||||
const globalMetrics = getContext("globalMetrics")
|
configName,
|
||||||
const dispatch = createEventDispatcher();
|
applyMetrics
|
||||||
|
} = $props();
|
||||||
let newMetricsOrder = [];
|
|
||||||
let unorderedMetrics = [...metrics];
|
|
||||||
let pendingShowFootprint = !!showFootprint;
|
|
||||||
|
|
||||||
onInit(() => {
|
|
||||||
if (allMetrics == null) allMetrics = new Set();
|
|
||||||
for (let metric of globalMetrics) allMetrics.add(metric.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (allMetrics != null) {
|
|
||||||
if (!cluster) {
|
|
||||||
for (let metric of globalMetrics) allMetrics.add(metric.name);
|
|
||||||
} else {
|
|
||||||
allMetrics.clear();
|
|
||||||
for (let gm of globalMetrics) {
|
|
||||||
if (!subCluster) {
|
|
||||||
if (gm.availability.find((av) => av.cluster === cluster)) allMetrics.add(gm.name);
|
|
||||||
} else {
|
|
||||||
if (gm.availability.find((av) => av.cluster === cluster && av.subClusters.includes(subCluster))) allMetrics.add(gm.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newMetricsOrder = [...allMetrics].filter((m) => !metrics.includes(m));
|
|
||||||
newMetricsOrder.unshift(...metrics.filter((m) => allMetrics.has(m)));
|
|
||||||
unorderedMetrics = unorderedMetrics.filter((m) => allMetrics.has(m));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printAvailability(metric, cluster) {
|
|
||||||
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
|
|
||||||
if (!cluster) {
|
|
||||||
return avail.map((av) => av.cluster).join(',')
|
|
||||||
} else {
|
|
||||||
return avail.find((av) => av.cluster === cluster).subClusters.join(',')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
|
const globalMetrics = getContext("globalMetrics");
|
||||||
|
const initialized = getContext("initialized");
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
return mutationStore({
|
return mutationStore({
|
||||||
@@ -87,7 +53,51 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let columnHovering = null;
|
/* State Init */
|
||||||
|
let pendingMetrics = $state(presetMetrics);
|
||||||
|
let pendingShowFootprint = $state(!!showFootprint);
|
||||||
|
let listedMetrics = $state([]);
|
||||||
|
let columnHovering = $state(null);
|
||||||
|
|
||||||
|
/* Derives States */
|
||||||
|
const allMetrics = $derived(loadAvailable(preInitialized || $initialized));
|
||||||
|
|
||||||
|
/* Reactive Effects */
|
||||||
|
$effect(() => {
|
||||||
|
totalMetrics = allMetrics.size;
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
listedMetrics = [...presetMetrics, ...allMetrics.difference(new Set(presetMetrics))]; // List (preset) active metrics first, then list inactives
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function loadAvailable(init) {
|
||||||
|
const availableMetrics = new Set();
|
||||||
|
if (init) {
|
||||||
|
for (let gm of globalMetrics) {
|
||||||
|
if (!cluster) {
|
||||||
|
availableMetrics.add(gm.name)
|
||||||
|
} else {
|
||||||
|
if (!subCluster) {
|
||||||
|
if (gm.availability.find((av) => av.cluster === cluster)) availableMetrics.add(gm.name);
|
||||||
|
} else {
|
||||||
|
if (gm.availability.find((av) => av.cluster === cluster && av.subClusters.includes(subCluster))) availableMetrics.add(gm.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return availableMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
function printAvailability(metric, cluster) {
|
||||||
|
const avail = globalMetrics.find((gm) => gm.name === metric)?.availability
|
||||||
|
if (!cluster) {
|
||||||
|
return avail.map((av) => av.cluster).join(',')
|
||||||
|
} else {
|
||||||
|
return avail.find((av) => av.cluster === cluster).subClusters.join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function columnsDragStart(event, i) {
|
function columnsDragStart(event, i) {
|
||||||
event.dataTransfer.effectAllowed = "move";
|
event.dataTransfer.effectAllowed = "move";
|
||||||
@@ -98,18 +108,21 @@
|
|||||||
function columnsDrag(event, target) {
|
function columnsDrag(event, target) {
|
||||||
event.dataTransfer.dropEffect = "move";
|
event.dataTransfer.dropEffect = "move";
|
||||||
const start = Number.parseInt(event.dataTransfer.getData("text/plain"));
|
const start = Number.parseInt(event.dataTransfer.getData("text/plain"));
|
||||||
|
|
||||||
|
let pendingMetricsOrder = [...listedMetrics];
|
||||||
if (start < target) {
|
if (start < target) {
|
||||||
newMetricsOrder.splice(target + 1, 0, newMetricsOrder[start]);
|
pendingMetricsOrder.splice(target + 1, 0, listedMetrics[start]);
|
||||||
newMetricsOrder.splice(start, 1);
|
pendingMetricsOrder.splice(start, 1);
|
||||||
} else {
|
} else {
|
||||||
newMetricsOrder.splice(target, 0, newMetricsOrder[start]);
|
pendingMetricsOrder.splice(target, 0, listedMetrics[start]);
|
||||||
newMetricsOrder.splice(start + 1, 1);
|
pendingMetricsOrder.splice(start + 1, 1);
|
||||||
}
|
}
|
||||||
|
listedMetrics = [...pendingMetricsOrder];
|
||||||
columnHovering = null;
|
columnHovering = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAndApply() {
|
function closeAndApply() {
|
||||||
metrics = newMetricsOrder.filter((m) => unorderedMetrics.includes(m));
|
pendingMetrics = listedMetrics.filter((m) => pendingMetrics.includes(m));
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
|
|
||||||
let configKey;
|
let configKey;
|
||||||
@@ -123,7 +136,7 @@
|
|||||||
|
|
||||||
updateConfigurationMutation({
|
updateConfigurationMutation({
|
||||||
name: configKey,
|
name: configKey,
|
||||||
value: JSON.stringify(metrics),
|
value: JSON.stringify(pendingMetrics),
|
||||||
}).subscribe((res) => {
|
}).subscribe((res) => {
|
||||||
if (res.fetching === false && res.error) {
|
if (res.fetching === false && res.error) {
|
||||||
throw res.error;
|
throw res.error;
|
||||||
@@ -145,7 +158,7 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch('update-metrics', metrics);
|
applyMetrics(pendingMetrics);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -159,27 +172,35 @@
|
|||||||
</li>
|
</li>
|
||||||
<hr />
|
<hr />
|
||||||
{/if}
|
{/if}
|
||||||
{#each newMetricsOrder as metric, index (metric)}
|
{#each listedMetrics as metric, index (metric)}
|
||||||
<li
|
<li
|
||||||
class="cc-config-column list-group-item"
|
class="cc-config-column list-group-item"
|
||||||
draggable={true}
|
draggable={true}
|
||||||
ondragover="return false"
|
ondragover={(event) => {
|
||||||
on:dragstart={(event) => columnsDragStart(event, index)}
|
event.preventDefault()
|
||||||
on:drop|preventDefault={(event) => columnsDrag(event, index)}
|
return false
|
||||||
on:dragenter={() => (columnHovering = index)}
|
}}
|
||||||
|
ondragstart={(event) => {
|
||||||
|
columnsDragStart(event, index)
|
||||||
|
}}
|
||||||
|
ondrop={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
columnsDrag(event, index)
|
||||||
|
}}
|
||||||
|
ondragenter={() => (columnHovering = index)}
|
||||||
class:is-active={columnHovering === index}
|
class:is-active={columnHovering === index}
|
||||||
>
|
>
|
||||||
{#if unorderedMetrics.includes(metric)}
|
{#if pendingMetrics.includes(metric)}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:group={unorderedMetrics}
|
bind:group={pendingMetrics}
|
||||||
value={metric}
|
value={metric}
|
||||||
checked
|
checked
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:group={unorderedMetrics}
|
bind:group={pendingMetrics}
|
||||||
value={metric}
|
value={metric}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -192,8 +213,8 @@
|
|||||||
</ListGroup>
|
</ListGroup>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button color="primary" on:click={closeAndApply}>Close & Apply</Button>
|
<Button color="primary" onclick={() => closeAndApply()}>Close & Apply</Button>
|
||||||
<Button color="secondary" on:click={() => (isOpen = !isOpen)}>Cancel</Button>
|
<Button color="secondary" onclick={() => (isOpen = !isOpen)}>Cancel</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
import { getContext, onMount } from "svelte";
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Button,
|
Button,
|
||||||
@@ -18,44 +18,73 @@
|
|||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { getSortItems } from "../utils.js";
|
|
||||||
|
|
||||||
export let isOpen = false;
|
/* Svelte 5 Props */
|
||||||
export let sorting = { field: "startTime", type: "col", order: "DESC" };
|
let {
|
||||||
|
isOpen = $bindable(false),
|
||||||
let sortableColumns = [];
|
presetSorting = { field: "startTime", type: "col", order: "DESC" },
|
||||||
let activeColumnIdx;
|
applySorting
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const initialized = getContext("initialized");
|
const initialized = getContext("initialized");
|
||||||
|
const globalMetrics = getContext("globalMetrics");
|
||||||
function loadSortables(isInitialized) {
|
const fixedSortables = $state([
|
||||||
if (!isInitialized) return;
|
{ field: "startTime", type: "col", text: "Start Time (Default)", order: "DESC" },
|
||||||
sortableColumns = [
|
{ field: "duration", type: "col", text: "Duration", order: "DESC" },
|
||||||
{ field: "startTime", type: "col", text: "Start Time", order: "DESC" },
|
{ field: "numNodes", type: "col", text: "Number of Nodes", order: "DESC" },
|
||||||
{ field: "duration", type: "col", text: "Duration", order: "DESC" },
|
{ field: "numHwthreads", type: "col", text: "Number of HWThreads", order: "DESC" },
|
||||||
{ field: "numNodes", type: "col", text: "Number of Nodes", order: "DESC" },
|
{ field: "numAcc", type: "col", text: "Number of Accelerators", order: "DESC" },
|
||||||
{ field: "numHwthreads", type: "col", text: "Number of HWThreads", order: "DESC" },
|
{ field: "energy", type: "col", text: "Total Energy", order: "DESC" },
|
||||||
{ field: "numAcc", type: "col", text: "Number of Accelerators", order: "DESC" },
|
]);
|
||||||
{ field: "energy", type: "col", text: "Total Energy", order: "DESC" },
|
|
||||||
...getSortItems()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadActiveIndex(isInitialized) {
|
/* State Init */
|
||||||
if (!isInitialized) return;
|
let sorting = $state({...presetSorting})
|
||||||
|
let activeColumnIdx = $state(0);
|
||||||
|
let metricSortables = $state([]);
|
||||||
|
|
||||||
|
/* Derived */
|
||||||
|
let sortableColumns = $derived([...fixedSortables, ...metricSortables]);
|
||||||
|
|
||||||
|
/* Effect */
|
||||||
|
$effect(() => {
|
||||||
|
if ($initialized) {
|
||||||
|
loadMetricSortables();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function loadMetricSortables() {
|
||||||
|
metricSortables = globalMetrics.map((gm) => {
|
||||||
|
if (gm?.footprint) {
|
||||||
|
return {
|
||||||
|
field: gm.name + '_' + gm.footprint,
|
||||||
|
type: 'foot',
|
||||||
|
text: gm.name + ' (' + gm.footprint + ')',
|
||||||
|
order: 'DESC'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}).filter((r) => r != null)
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadActiveIndex() {
|
||||||
activeColumnIdx = sortableColumns.findIndex(
|
activeColumnIdx = sortableColumns.findIndex(
|
||||||
(col) => col.field == sorting.field,
|
(col) => col.field == sorting.field,
|
||||||
);
|
);
|
||||||
sortableColumns[activeColumnIdx].order = sorting.order;
|
sortableColumns[activeColumnIdx].order = sorting.order;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: loadSortables($initialized);
|
function resetSorting(sort) {
|
||||||
$: loadActiveIndex($initialized)
|
sorting = {...sort};
|
||||||
|
loadActiveIndex();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
{isOpen}
|
{isOpen}
|
||||||
toggle={() => {
|
toggle={() => {
|
||||||
|
resetSorting(presetSorting);
|
||||||
isOpen = !isOpen;
|
isOpen = !isOpen;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -66,7 +95,7 @@
|
|||||||
<ListGroupItem>
|
<ListGroupItem>
|
||||||
<button
|
<button
|
||||||
class="sort"
|
class="sort"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
if (activeColumnIdx == i) {
|
if (activeColumnIdx == i) {
|
||||||
col.order = col.order == "DESC" ? "ASC" : "DESC";
|
col.order = col.order == "DESC" ? "ASC" : "DESC";
|
||||||
} else {
|
} else {
|
||||||
@@ -96,11 +125,27 @@
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="warning"
|
||||||
on:click={() => {
|
onclick={() => {
|
||||||
isOpen = false;
|
isOpen = false;
|
||||||
}}>Close</Button
|
resetSorting({ field: "startTime", type: "col", order: "DESC" });
|
||||||
|
applySorting(sorting);
|
||||||
|
}}>Reset</Button
|
||||||
>
|
>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onclick={() => {
|
||||||
|
applySorting(sorting);
|
||||||
|
isOpen = false;
|
||||||
|
}}>Close & Apply</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onclick={() => {
|
||||||
|
resetSorting(presetSorting);
|
||||||
|
isOpen = false
|
||||||
|
}}>Cancel
|
||||||
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
@@ -20,78 +19,96 @@
|
|||||||
InputGroupText,
|
InputGroupText,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let from;
|
/* Svelte 5 Props */
|
||||||
export let to;
|
let {
|
||||||
export let customEnabled = true;
|
presetFrom,
|
||||||
export let options = {
|
presetTo,
|
||||||
"Last quarter hour": 15 * 60,
|
customEnabled = true,
|
||||||
"Last half hour": 30 * 60,
|
options = {
|
||||||
"Last hour": 60 * 60,
|
"Last quarter hour": 15 * 60,
|
||||||
"Last 2hrs": 2 * 60 * 60,
|
"Last half hour": 30 * 60,
|
||||||
"Last 4hrs": 4 * 60 * 60,
|
"Last hour": 60 * 60,
|
||||||
"Last 12hrs": 12 * 60 * 60,
|
"Last 2hrs": 2 * 60 * 60,
|
||||||
"Last 24hrs": 24 * 60 * 60,
|
"Last 4hrs": 4 * 60 * 60,
|
||||||
|
"Last 12hrs": 12 * 60 * 60,
|
||||||
|
"Last 24hrs": 24 * 60 * 60,
|
||||||
|
},
|
||||||
|
applyTime
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
|
const defaultTo = new Date(Date.now());
|
||||||
|
const defaultFrom = new Date(defaultTo.setHours(defaultTo.getHours() - 4));
|
||||||
|
|
||||||
|
/* State Init */
|
||||||
|
let timeType = $state("range");
|
||||||
|
let pendingCustomFrom = $state(null);
|
||||||
|
let pendingCustomTo = $state(null);
|
||||||
|
|
||||||
|
/* Derived */
|
||||||
|
let timeRange = $derived.by(() => {
|
||||||
|
if (presetTo && presetFrom) {
|
||||||
|
return ((presetTo.getTime() - presetFrom.getTime()) / 1000)
|
||||||
|
} else {
|
||||||
|
return ((defaultTo.getTime() - defaultFrom.getTime()) / 1000)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let unknownRange = $derived(!Object.values(options).includes(timeRange));
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function updateTimeRange() {
|
||||||
|
let now = Date.now();
|
||||||
|
let t = timeRange * 1000;
|
||||||
|
applyTime(new Date(now - t), new Date(now));
|
||||||
};
|
};
|
||||||
|
|
||||||
$: pendingFrom = from;
|
function updateTimeCustom() {
|
||||||
$: pendingTo = to;
|
if (pendingCustomFrom && pendingCustomTo) {
|
||||||
|
applyTime(new Date(pendingCustomFrom), new Date(pendingCustomTo));
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
let timeRange = // If both times set, return diff, else: display custom select
|
|
||||||
(to && from) ? ((to.getTime() - from.getTime()) / 1000) : -1;
|
|
||||||
|
|
||||||
function updateTimeRange() {
|
|
||||||
if (timeRange == -1) {
|
|
||||||
pendingFrom = null;
|
|
||||||
pendingTo = null;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
let now = Date.now(),
|
|
||||||
t = timeRange * 1000;
|
|
||||||
from = pendingFrom = new Date(now - t);
|
|
||||||
to = pendingTo = new Date(now);
|
|
||||||
dispatch("change", { from, to });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateExplicitTimeRange(type, event) {
|
|
||||||
let d = new Date(Date.parse(event.target.value));
|
|
||||||
if (type == "from") pendingFrom = d;
|
|
||||||
else pendingTo = d;
|
|
||||||
|
|
||||||
if (pendingFrom != null && pendingTo != null) {
|
|
||||||
from = pendingFrom;
|
|
||||||
to = pendingTo;
|
|
||||||
dispatch("change", { from, to });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InputGroup class="inline-from">
|
<InputGroup class="inline-from">
|
||||||
<InputGroupText><Icon name="clock-history" /></InputGroupText>
|
<InputGroupText><Icon name="clock-history" /></InputGroupText>
|
||||||
<InputGroupText>Range</InputGroupText>
|
{#if customEnabled}
|
||||||
<select
|
<Input
|
||||||
class="form-select"
|
type="select"
|
||||||
bind:value={timeRange}
|
style="max-width:fit-content;background-color:#f8f9fa;"
|
||||||
on:change={updateTimeRange}
|
bind:value={timeType}
|
||||||
>
|
>
|
||||||
{#if customEnabled}
|
<option value="range">Range</option>
|
||||||
<option value={-1}>Custom</option>
|
<option value="custom">Custom</option>
|
||||||
{/if}
|
</Input>
|
||||||
{#each Object.entries(options) as [name, seconds]}
|
{:else}
|
||||||
<option value={seconds}>{name}</option>
|
<InputGroupText>Range</InputGroupText>
|
||||||
{/each}
|
{/if}
|
||||||
</select>
|
|
||||||
{#if timeRange == -1}
|
{#if timeType === "range"}
|
||||||
|
<Input
|
||||||
|
type="select"
|
||||||
|
bind:value={timeRange}
|
||||||
|
onchange={updateTimeRange}
|
||||||
|
>
|
||||||
|
{#if unknownRange}
|
||||||
|
<option value={timeRange} disabled>Select new range...</option>
|
||||||
|
{/if}
|
||||||
|
{#each Object.entries(options) as [name, seconds]}
|
||||||
|
<option value={seconds}>{name}</option>
|
||||||
|
{/each}
|
||||||
|
</Input>
|
||||||
|
{:else}
|
||||||
<InputGroupText>from</InputGroupText>
|
<InputGroupText>from</InputGroupText>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
on:change={(event) => updateExplicitTimeRange("from", event)}
|
bind:value={pendingCustomFrom}
|
||||||
|
onchange={updateTimeCustom}
|
||||||
></Input>
|
></Input>
|
||||||
<InputGroupText>to</InputGroupText>
|
<InputGroupText>to</InputGroupText>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
on:change={(event) => updateExplicitTimeRange("to", event)}
|
bind:value={pendingCustomTo}
|
||||||
|
onchange={updateTimeCustom}
|
||||||
></Input>
|
></Input>
|
||||||
{/if}
|
{/if}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|||||||
@@ -388,24 +388,6 @@ export function findJobFootprintThresholds(job, stat, metricConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSortItems() {
|
|
||||||
//console.time('sort')
|
|
||||||
const globalMetrics = getContext("globalMetrics")
|
|
||||||
const result = globalMetrics.map((gm) => {
|
|
||||||
if (gm?.footprint) {
|
|
||||||
return {
|
|
||||||
field: gm.name + '_' + gm.footprint,
|
|
||||||
type: 'foot',
|
|
||||||
text: gm.name + ' (' + gm.footprint + ')',
|
|
||||||
order: 'DESC'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}).filter((r) => r != null)
|
|
||||||
//console.timeEnd('sort')
|
|
||||||
return [...result];
|
|
||||||
};
|
|
||||||
|
|
||||||
function getMetricConfigDeep(metric, cluster, subCluster) {
|
function getMetricConfigDeep(metric, cluster, subCluster) {
|
||||||
const clusters = getContext("clusters");
|
const clusters = getContext("clusters");
|
||||||
if (cluster != null) {
|
if (cluster != null) {
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import Header from './Header.svelte'
|
import { mount } from 'svelte';
|
||||||
|
import Header from './Header.svelte';
|
||||||
|
|
||||||
const headerDomTarget = document.getElementById('svelte-header')
|
const headerDomTarget = document.getElementById('svelte-header');
|
||||||
|
|
||||||
if (headerDomTarget != null) {
|
if (headerDomTarget != null) {
|
||||||
new Header({
|
mount(Header, {
|
||||||
target: headerDomTarget,
|
target: headerDomTarget,
|
||||||
props: { ...header },
|
props: { // { ...header },
|
||||||
})
|
username: hUsername,
|
||||||
|
authlevel: hAuthlevel,
|
||||||
|
clusters: hClusters,
|
||||||
|
subClusters: hSubClusters,
|
||||||
|
roles: hRoles,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,13 @@
|
|||||||
DropdownItem,
|
DropdownItem,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let clusters;
|
/* Svelte 5 Props */
|
||||||
export let subClusters;
|
let {
|
||||||
export let links;
|
clusters,
|
||||||
export let direction = "down";
|
subClusters,
|
||||||
|
links,
|
||||||
|
direction = "down"
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each links as item}
|
{#each links as item}
|
||||||
|
|||||||
@@ -25,10 +25,13 @@
|
|||||||
Col,
|
Col,
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let username;
|
/* Svelte 5 Props */
|
||||||
export let authlevel;
|
let {
|
||||||
export let roles;
|
username,
|
||||||
export let screenSize;
|
authlevel,
|
||||||
|
roles,
|
||||||
|
screenSize
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Nav navbar>
|
<Nav navbar>
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
<NavItem>
|
<NavItem>
|
||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
on:click={() => (window.location.href = "/config")}
|
onclick={() => (window.location.href = "/config")}
|
||||||
style="margin-left: 10px;"
|
style="margin-left: 10px;"
|
||||||
title="Settings"
|
title="Settings"
|
||||||
>
|
>
|
||||||
@@ -136,7 +139,7 @@
|
|||||||
<Col xs="4">
|
<Col xs="4">
|
||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
on:click={() => (window.location.href = "/config")}
|
onclick={() => (window.location.href = "/config")}
|
||||||
size="sm"
|
size="sm"
|
||||||
class="my-2 w-100"
|
class="my-2 w-100"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import Job from './Job.root.svelte'
|
import Job from './Job.root.svelte'
|
||||||
|
|
||||||
new Job({
|
mount(Job, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
dbid: jobInfos.id,
|
dbid: jobInfos.id,
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
getContext
|
getContext,
|
||||||
|
onMount
|
||||||
} from "svelte";
|
} from "svelte";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -21,17 +22,26 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { round } from "mathjs";
|
import { round } from "mathjs";
|
||||||
|
|
||||||
export let jobId;
|
/* Svelte 5 Props */
|
||||||
export let jobEnergy = null;
|
let {
|
||||||
export let jobEnergyFootprint = null;
|
jobId,
|
||||||
|
jobEnergy = null,
|
||||||
|
jobEnergyFootprint = null
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const carbonPerkWh = getContext("emission");
|
const carbonPerkWh = getContext("emission");
|
||||||
let carbonMass;
|
|
||||||
|
/* State Init */
|
||||||
|
let carbonMass = $state(0);
|
||||||
|
|
||||||
$: if (carbonPerkWh) {
|
/* On Mount */
|
||||||
// ( kWh * g/kWh) / 1000 = kg || Rounded to 2 Digits via [ round(x * 100) / 100 ]
|
onMount(() => {
|
||||||
carbonMass = round( ((jobEnergy ? jobEnergy : 0.0) * carbonPerkWh) / 10 ) / 100;
|
if (carbonPerkWh) {
|
||||||
}
|
// ( kWh * g/kWh) / 1000 = kg || Rounded to 2 Digits via [ round(x * 100) / 100 ]
|
||||||
|
carbonMass = round( ((jobEnergy ? jobEnergy : 0.0) * carbonPerkWh) / 10 ) / 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -7,73 +7,78 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
queryStore,
|
queryStore,
|
||||||
gql,
|
gql,
|
||||||
getContextClient
|
getContextClient
|
||||||
} from "@urql/svelte";
|
} from "@urql/svelte";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Spinner
|
Spinner
|
||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import {
|
import {
|
||||||
transformDataForRoofline,
|
transformDataForRoofline,
|
||||||
} from "../generic/utils.js";
|
} from "../generic/utils.js";
|
||||||
import Roofline from "../generic/plots/Roofline.svelte";
|
import Roofline from "../generic/plots/Roofline.svelte";
|
||||||
|
|
||||||
export let job;
|
/* Svelte 5 Props */
|
||||||
export let clusters;
|
let {
|
||||||
|
job,
|
||||||
let roofWidth;
|
clusters,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const client = getContextClient();
|
/* Const Init */
|
||||||
const roofQuery = gql`
|
const client = getContextClient();
|
||||||
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
|
const roofQuery = gql`
|
||||||
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
|
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
|
||||||
name
|
jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
|
||||||
scope
|
name
|
||||||
metric {
|
scope
|
||||||
series {
|
metric {
|
||||||
data
|
series {
|
||||||
}
|
data
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`;
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
// Roofline: Always load roofMetrics with configured timestep (Resolution: 0)
|
/* State Init */
|
||||||
$: roofMetrics = queryStore({
|
let roofWidth = $state(0);
|
||||||
client: client,
|
|
||||||
query: roofQuery,
|
|
||||||
variables: { dbid: job.id, selectedMetrics: ["flops_any", "mem_bw"], selectedScopes: ["node"], selectedResolution: 0 },
|
|
||||||
});
|
|
||||||
|
|
||||||
|
/* Derived */
|
||||||
|
// Roofline: Always load roofMetrics with configured timestep (Resolution: 0)
|
||||||
|
const roofMetrics = $derived(queryStore({
|
||||||
|
client: client,
|
||||||
|
query: roofQuery,
|
||||||
|
variables: { dbid: job.id, selectedMetrics: ["flops_any", "mem_bw"], selectedScopes: ["node"], selectedResolution: 0 },
|
||||||
|
})
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $roofMetrics.error}
|
{#if $roofMetrics.error}
|
||||||
<Card body color="danger">{$roofMetrics.error.message}</Card>
|
<Card body color="danger">{$roofMetrics.error.message}</Card>
|
||||||
{:else if $roofMetrics?.data}
|
{:else if $roofMetrics?.data}
|
||||||
<Card style="height: 400px;">
|
<Card style="height: 400px;">
|
||||||
<div bind:clientWidth={roofWidth}>
|
<div bind:clientWidth={roofWidth}>
|
||||||
<Roofline
|
<Roofline
|
||||||
width={roofWidth}
|
width={roofWidth}
|
||||||
subCluster={clusters
|
subCluster={clusters
|
||||||
.find((c) => c.name == job.cluster)
|
.find((c) => c.name == job.cluster)
|
||||||
.subClusters.find((sc) => sc.name == job.subCluster)}
|
.subClusters.find((sc) => sc.name == job.subCluster)}
|
||||||
data={transformDataForRoofline(
|
data={transformDataForRoofline(
|
||||||
$roofMetrics.data?.jobMetrics?.find(
|
$roofMetrics.data?.jobMetrics?.find(
|
||||||
(m) => m.name == "flops_any" && m.scope == "node",
|
(m) => m.name == "flops_any" && m.scope == "node",
|
||||||
)?.metric,
|
)?.metric,
|
||||||
$roofMetrics.data?.jobMetrics?.find(
|
$roofMetrics.data?.jobMetrics?.find(
|
||||||
(m) => m.name == "mem_bw" && m.scope == "node",
|
(m) => m.name == "mem_bw" && m.scope == "node",
|
||||||
)?.metric,
|
)?.metric,
|
||||||
)}
|
)}
|
||||||
allowSizeChange
|
allowSizeChange
|
||||||
renderTime
|
renderTime
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{:else}
|
{:else}
|
||||||
<Spinner secondary />
|
<Spinner secondary />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,12 @@
|
|||||||
import JobFootprintPolar from "./jobsummary/JobFootprintPolar.svelte";
|
import JobFootprintPolar from "./jobsummary/JobFootprintPolar.svelte";
|
||||||
|
|
||||||
|
|
||||||
export let job;
|
/* Svelte 5 Props */
|
||||||
export let width = "auto";
|
let {
|
||||||
export let height = "400px";
|
job,
|
||||||
|
width = "auto",
|
||||||
|
height = "400px",
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`];
|
const showFootprintTab = !!getContext("cc-config")[`job_view_showFootprint`];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -26,16 +26,14 @@
|
|||||||
import MetricSelection from "../generic/select/MetricSelection.svelte";
|
import MetricSelection from "../generic/select/MetricSelection.svelte";
|
||||||
import StatsTable from "./statstab/StatsTable.svelte";
|
import StatsTable from "./statstab/StatsTable.svelte";
|
||||||
|
|
||||||
export let job;
|
/* Svelte 5 Props */
|
||||||
export let clusters;
|
let {
|
||||||
export let tabActive;
|
job,
|
||||||
|
clusters,
|
||||||
let loadScopes = false;
|
tabActive,
|
||||||
let selectedScopes = [];
|
} = $props();
|
||||||
let selectedMetrics = [];
|
|
||||||
let availableMetrics = new Set(); // For Info Only, filled by MetricSelection Component
|
|
||||||
let isMetricSelectionOpen = false;
|
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const query = gql`
|
const query = gql`
|
||||||
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
|
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!) {
|
||||||
@@ -55,16 +53,29 @@
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
$: scopedStats = queryStore({
|
/* State Init */
|
||||||
client: client,
|
let moreScopes = $state(false);
|
||||||
query: query,
|
let selectedScopes = $state([]);
|
||||||
variables: { dbid: job.id, selectedMetrics, selectedScopes },
|
let selectedMetrics = $state([]);
|
||||||
});
|
let totalMetrics = $state(0); // For Info Only, filled by MetricSelection Component
|
||||||
|
let isMetricSelectionOpen = $state(false);
|
||||||
|
|
||||||
$: if (loadScopes) {
|
/* Derived */
|
||||||
|
const scopedStats = $derived(queryStore({
|
||||||
|
client: client,
|
||||||
|
query: query,
|
||||||
|
variables: { dbid: job.id, selectedMetrics, selectedScopes },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function loadScopes() {
|
||||||
|
// Archived Jobs Load All Scopes By Default (See Backend)
|
||||||
|
moreScopes = true;
|
||||||
selectedScopes = ["node", "socket", "core", "hwthread", "accelerator"];
|
selectedScopes = ["node", "socket", "core", "hwthread", "accelerator"];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/* On Init */
|
||||||
// Handle Job Query on Init -> is not executed anymore
|
// Handle Job Query on Init -> is not executed anymore
|
||||||
getContext("on-init")(() => {
|
getContext("on-init")(() => {
|
||||||
if (!job) return;
|
if (!job) return;
|
||||||
@@ -98,12 +109,12 @@
|
|||||||
<TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}>
|
<TabPane tabId="stats" tab="Statistics Table" class="overflow-x-auto" active={tabActive}>
|
||||||
<Row>
|
<Row>
|
||||||
<Col class="m-2">
|
<Col class="m-2">
|
||||||
<Button outline on:click={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem">
|
<Button outline onclick={() => (isMetricSelectionOpen = true)} class="px-2" color="primary" style="margin-right:0.5rem">
|
||||||
Select Metrics (Selected {selectedMetrics.length} of {availableMetrics.size} available)
|
Select Metrics (Selected {selectedMetrics.length} of {totalMetrics} available)
|
||||||
</Button>
|
</Button>
|
||||||
{#if job.numNodes > 1}
|
{#if job.numNodes > 1 && job.state === "running"}
|
||||||
<Button class="px-2 ml-auto" color="success" outline on:click={() => (loadScopes = !loadScopes)} disabled={loadScopes}>
|
<Button class="px-2 ml-auto" color="success" outline onclick={loadScopes} disabled={moreScopes}>
|
||||||
{#if !loadScopes}
|
{#if !moreScopes}
|
||||||
<Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes
|
<Icon name="plus-square-fill" style="margin-right:0.25rem"/> Add More Scopes
|
||||||
{:else}
|
{:else}
|
||||||
<Icon name="check-square-fill" style="margin-right:0.25rem"/> OK: Scopes Added
|
<Icon name="check-square-fill" style="margin-right:0.25rem"/> OK: Scopes Added
|
||||||
@@ -129,17 +140,21 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<StatsTable
|
<StatsTable
|
||||||
hosts={job.resources.map((r) => r.hostname).sort()}
|
hosts={job.resources.map((r) => r.hostname).sort()}
|
||||||
data={$scopedStats?.data?.scopedJobStats}
|
jobStats={$scopedStats?.data?.scopedJobStats}
|
||||||
{selectedMetrics}
|
{selectedMetrics}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<MetricSelection
|
<MetricSelection
|
||||||
|
bind:isOpen={isMetricSelectionOpen}
|
||||||
|
bind:totalMetrics
|
||||||
|
presetMetrics={selectedMetrics}
|
||||||
cluster={job.cluster}
|
cluster={job.cluster}
|
||||||
subCluster={job.subCluster}
|
subCluster={job.subCluster}
|
||||||
configName="job_view_nodestats_selectedMetrics"
|
configName="job_view_nodestats_selectedMetrics"
|
||||||
bind:allMetrics={availableMetrics}
|
preInitialized
|
||||||
bind:metrics={selectedMetrics}
|
applyMetrics={(newMetrics) =>
|
||||||
bind:isOpen={isMetricSelectionOpen}
|
selectedMetrics = [...newMetrics]
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,79 +17,88 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import { findJobFootprintThresholds } from "../../generic/utils.js";
|
import { findJobFootprintThresholds } from "../../generic/utils.js";
|
||||||
|
|
||||||
export let job;
|
/* Svelte 5 Props */
|
||||||
|
let {job} = $props();
|
||||||
|
|
||||||
|
/* Derived */
|
||||||
// Prepare Job Footprint Data Based On Values Saved In Database
|
// Prepare Job Footprint Data Based On Values Saved In Database
|
||||||
const jobFootprintData = job?.footprint?.map((jf) => {
|
const jobFootprintData = $derived(buildFootprint(job?.footprint));
|
||||||
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
|
|
||||||
if (fmc) {
|
/* Functions */
|
||||||
// Unit
|
function buildFootprint(input) {
|
||||||
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
|
let result = input?.map((jf) => {
|
||||||
|
const fmc = getContext("getMetricConfig")(job.cluster, job.subCluster, jf.name);
|
||||||
// Threshold / -Differences
|
if (fmc) {
|
||||||
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
|
// Unit
|
||||||
|
const unit = (fmc?.unit?.prefix ? fmc.unit.prefix : "") + (fmc?.unit?.base ? fmc.unit.base : "")
|
||||||
// Define basic data -> Value: Use as Provided
|
|
||||||
const fmBase = {
|
// Threshold / -Differences
|
||||||
name: jf.name,
|
const fmt = findJobFootprintThresholds(job, jf.stat, fmc);
|
||||||
stat: jf.stat,
|
|
||||||
value: jf.value,
|
// Define basic data -> Value: Use as Provided
|
||||||
unit: unit,
|
const fmBase = {
|
||||||
peak: fmt.peak,
|
name: jf.name,
|
||||||
dir: fmc.lowerIsBetter
|
stat: jf.stat,
|
||||||
};
|
value: jf.value,
|
||||||
|
unit: unit,
|
||||||
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
|
peak: fmt.peak,
|
||||||
return {
|
dir: fmc.lowerIsBetter
|
||||||
...fmBase,
|
|
||||||
color: "danger",
|
|
||||||
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
|
||||||
impact: 3
|
|
||||||
};
|
};
|
||||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
|
|
||||||
|
if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "alert")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "danger",
|
||||||
|
message: `Footprint value way ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
||||||
|
impact: 3
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "caution")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "warning",
|
||||||
|
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
||||||
|
impact: 2,
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "success",
|
||||||
|
message: "Footprint value within expected thresholds.",
|
||||||
|
impact: 1,
|
||||||
|
};
|
||||||
|
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "info",
|
||||||
|
message:
|
||||||
|
"Footprint value above expected normal threshold: Check for artifacts recommended.",
|
||||||
|
impact: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...fmBase,
|
||||||
|
color: "secondary",
|
||||||
|
message:
|
||||||
|
"Footprint value above expected peak threshold: Check for artifacts!",
|
||||||
|
impact: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else { // No matching metric config: display as single value
|
||||||
return {
|
return {
|
||||||
...fmBase,
|
name: jf.name,
|
||||||
color: "warning",
|
stat: jf.stat,
|
||||||
message: `Footprint value ${fmc.lowerIsBetter ? "above" : "below"} expected normal threshold.`,
|
value: jf.value,
|
||||||
impact: 2,
|
|
||||||
};
|
|
||||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "normal")) {
|
|
||||||
return {
|
|
||||||
...fmBase,
|
|
||||||
color: "success",
|
|
||||||
message: "Footprint value within expected thresholds.",
|
|
||||||
impact: 1,
|
|
||||||
};
|
|
||||||
} else if (evalFootprint(jf.value, fmt, fmc.lowerIsBetter, "peak")) {
|
|
||||||
return {
|
|
||||||
...fmBase,
|
|
||||||
color: "info",
|
|
||||||
message:
|
message:
|
||||||
"Footprint value above expected normal threshold: Check for artifacts recommended.",
|
`No config for metric ${jf.name} found.`,
|
||||||
impact: 0,
|
impact: 4,
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...fmBase,
|
|
||||||
color: "secondary",
|
|
||||||
message:
|
|
||||||
"Footprint value above expected peak threshold: Check for artifacts!",
|
|
||||||
impact: -1,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else { // No matching metric config: display as single value
|
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
|
||||||
return {
|
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
|
||||||
name: jf.name,
|
});;
|
||||||
stat: jf.stat,
|
|
||||||
value: jf.value,
|
return result
|
||||||
message:
|
};
|
||||||
`No config for metric ${jf.name} found.`,
|
|
||||||
impact: 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}).sort(function (a, b) { // Sort by impact value primarily, within impact sort name alphabetically
|
|
||||||
return a.impact - b.impact || ((a.name > b.name) ? 1 : ((b.name > a.name) ? -1 : 0));
|
|
||||||
});;
|
|
||||||
|
|
||||||
function evalFootprint(value, thresholds, lowerIsBetter, level) {
|
function evalFootprint(value, thresholds, lowerIsBetter, level) {
|
||||||
// Handle Metrics in which less value is better
|
// Handle Metrics in which less value is better
|
||||||
@@ -161,7 +170,7 @@
|
|||||||
>{fpd.message}</Tooltip
|
>{fpd.message}</Tooltip
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<Row cols={12} class="{(jobFootprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}">
|
<Row cols={12} class={(jobFootprintData.length == (index + 1)) ? 'mb-0' : 'mb-2'}>
|
||||||
{#if fpd.dir}
|
{#if fpd.dir}
|
||||||
<Col xs="1">
|
<Col xs="1">
|
||||||
<Icon name="caret-left-fill" />
|
<Icon name="caret-left-fill" />
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
import Polar from "../../generic/plots/Polar.svelte";
|
import Polar from "../../generic/plots/Polar.svelte";
|
||||||
import { findJobFootprintThresholds } from "../../generic/utils.js";
|
import { findJobFootprintThresholds } from "../../generic/utils.js";
|
||||||
|
|
||||||
export let job;
|
/* Svelte 5 Props */
|
||||||
|
let { job } = $props();
|
||||||
|
|
||||||
|
/* Const Init */
|
||||||
// Metric Names Configured To Be Footprints For (sub)Cluster
|
// Metric Names Configured To Be Footprints For (sub)Cluster
|
||||||
const clusterFootprintMetrics = getContext("clusters")
|
const clusterFootprintMetrics = getContext("clusters")
|
||||||
.find((c) => c.name == job.cluster)?.subClusters
|
.find((c) => c.name == job.cluster)?.subClusters
|
||||||
@@ -39,23 +41,24 @@
|
|||||||
// Pull All Series For Footprint Metrics Statistics Only On Node Scope
|
// Pull All Series For Footprint Metrics Statistics Only On Node Scope
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const polarQuery = gql`
|
const polarQuery = gql`
|
||||||
query ($dbid: ID!, $selectedMetrics: [String!]!) {
|
query ($dbid: ID!, $selectedMetrics: [String!]!) {
|
||||||
jobStats(id: $dbid, metrics: $selectedMetrics) {
|
jobStats(id: $dbid, metrics: $selectedMetrics) {
|
||||||
name
|
name
|
||||||
data {
|
data {
|
||||||
min
|
min
|
||||||
avg
|
avg
|
||||||
max
|
max
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
$: polarData = queryStore({
|
/* Derived */
|
||||||
|
const polarData = $derived(queryStore({
|
||||||
client: client,
|
client: client,
|
||||||
query: polarQuery,
|
query: polarQuery,
|
||||||
variables:{ dbid: job.id, selectedMetrics: clusterFootprintMetrics },
|
variables:{ dbid: job.id, selectedMetrics: clusterFootprintMetrics },
|
||||||
});
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
|
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `data Object`: The data object
|
|
||||||
- `selectedMetrics [String]`: The selected metrics
|
|
||||||
- `hosts [String]`: The list of hostnames of this job
|
- `hosts [String]`: The list of hostnames of this job
|
||||||
|
- `jobStats Object`: The data object
|
||||||
|
- `selectedMetrics [String]`: The selected metrics
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -17,39 +17,87 @@
|
|||||||
} from "@sveltestrap/sveltestrap";
|
} from "@sveltestrap/sveltestrap";
|
||||||
import StatsTableEntry from "./StatsTableEntry.svelte";
|
import StatsTableEntry from "./StatsTableEntry.svelte";
|
||||||
|
|
||||||
export let data = [];
|
/* Svelte 5 Props */
|
||||||
export let selectedMetrics = [];
|
let {
|
||||||
export let hosts = [];
|
hosts = [],
|
||||||
|
jobStats = [],
|
||||||
|
selectedMetrics = [],
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let sorting = {};
|
/* State Init */
|
||||||
let availableScopes = {};
|
let sortedHosts = $state(hosts);
|
||||||
let selectedScopes = {};
|
let sorting = $state(setupSorting(selectedMetrics));
|
||||||
|
let availableScopes = $state(setupAvailable(jobStats));
|
||||||
|
let selectedScopes = $state(setupSelected(availableScopes));
|
||||||
|
|
||||||
const scopesForMetric = (metric) =>
|
/* Derived Init */
|
||||||
data?.filter((jm) => jm.name == metric)?.map((jm) => jm.scope) || [];
|
const tableData = $derived(setupData(jobStats, hosts, selectedMetrics, availableScopes))
|
||||||
const setScopeForMetric = (metric, scope) =>
|
|
||||||
selectedScopes[metric] = scope
|
|
||||||
|
|
||||||
$: if (data && selectedMetrics) {
|
/* Functions */
|
||||||
for (let metric of selectedMetrics) {
|
function setupSorting(metrics) {
|
||||||
availableScopes[metric] = scopesForMetric(metric);
|
let pendingSorting = {};
|
||||||
// Set Initial Selection, but do not use selectedScopes: Skips reactivity
|
if (metrics) {
|
||||||
if (availableScopes[metric].includes("accelerator")) {
|
for (let metric of metrics) {
|
||||||
setScopeForMetric(metric, "accelerator");
|
pendingSorting[metric] = {
|
||||||
} else if (availableScopes[metric].includes("core")) {
|
min: { dir: "up", active: false },
|
||||||
setScopeForMetric(metric, "core");
|
avg: { dir: "up", active: false },
|
||||||
} else if (availableScopes[metric].includes("socket")) {
|
max: { dir: "up", active: false },
|
||||||
setScopeForMetric(metric, "socket");
|
};
|
||||||
} else {
|
|
||||||
setScopeForMetric(metric, "node");
|
|
||||||
}
|
|
||||||
|
|
||||||
sorting[metric] = {
|
|
||||||
min: { dir: "up", active: false },
|
|
||||||
avg: { dir: "up", active: false },
|
|
||||||
max: { dir: "up", active: false },
|
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
return pendingSorting;
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupAvailable(data) {
|
||||||
|
let pendingAvailable = {};
|
||||||
|
if (data) {
|
||||||
|
for (let d of data) {
|
||||||
|
if (!pendingAvailable[d.name]) {
|
||||||
|
pendingAvailable[d.name] = [d.scope]
|
||||||
|
} else {
|
||||||
|
pendingAvailable[d.name] = [...pendingAvailable[d.name], d.scope]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return pendingAvailable;
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupSelected(available) {
|
||||||
|
let pendingSelected = {};
|
||||||
|
for (const [metric, scopes] of Object.entries(available)) {
|
||||||
|
if (scopes.includes("accelerator")) {
|
||||||
|
pendingSelected[metric] = "accelerator"
|
||||||
|
} else if (scopes.includes("core")) {
|
||||||
|
pendingSelected[metric] = "core"
|
||||||
|
} else if (scopes.includes("socket")) {
|
||||||
|
pendingSelected[metric] = "socket"
|
||||||
|
} else {
|
||||||
|
pendingSelected[metric] = "node"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return pendingSelected;
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupData(js, h, sm, as) {
|
||||||
|
let pendingTableData = {};
|
||||||
|
if (js) {
|
||||||
|
for (const host of h) {
|
||||||
|
if (!pendingTableData[host]) {
|
||||||
|
pendingTableData[host] = {};
|
||||||
|
};
|
||||||
|
for (const metric of sm) {
|
||||||
|
if (!pendingTableData[host][metric]) {
|
||||||
|
pendingTableData[host][metric] = {};
|
||||||
|
};
|
||||||
|
for (const scope of as[metric]) {
|
||||||
|
pendingTableData[host][metric][scope] = js.find((d) => d.name == metric && d.scope == scope)
|
||||||
|
?.stats.filter((st) => st.hostname == host && st.data != null)
|
||||||
|
?.sort((a, b) => a.id - b.id) || []
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return pendingTableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortBy(metric, stat) {
|
function sortBy(metric, stat) {
|
||||||
@@ -62,11 +110,12 @@
|
|||||||
s.active = true;
|
s.active = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats = data.find(
|
let stats = jobStats.find(
|
||||||
(d) => d.name == metric && d.scope == "node",
|
(js) => js.name == metric && js.scope == "node",
|
||||||
)?.stats || [];
|
)?.stats || [];
|
||||||
sorting = { ...sorting };
|
sorting = { ...sorting };
|
||||||
hosts = hosts.sort((h1, h2) => {
|
|
||||||
|
sortedHosts = sortedHosts.sort((h1, h2) => {
|
||||||
let s1 = stats.find((s) => s.hostname == h1)?.data;
|
let s1 = stats.find((s) => s.hostname == h1)?.data;
|
||||||
let s2 = stats.find((s) => s.hostname == h2)?.data;
|
let s2 = stats.find((s) => s.hostname == h2)?.data;
|
||||||
if (s1 == null || s2 == null) return -1;
|
if (s1 == null || s2 == null) return -1;
|
||||||
@@ -81,7 +130,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<!-- Header Row 1: Selectors -->
|
<!-- Header Row 1: Selectors -->
|
||||||
<tr>
|
<tr>
|
||||||
<th/>
|
<th></th>
|
||||||
{#each selectedMetrics as metric}
|
{#each selectedMetrics as metric}
|
||||||
<!-- To Match Row-2 Header Field Count-->
|
<!-- To Match Row-2 Header Field Count-->
|
||||||
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
|
<th colspan={selectedScopes[metric] == "node" ? 3 : 4}>
|
||||||
@@ -89,7 +138,7 @@
|
|||||||
<InputGroupText>
|
<InputGroupText>
|
||||||
{metric}
|
{metric}
|
||||||
</InputGroupText>
|
</InputGroupText>
|
||||||
<Input type="select" bind:value={selectedScopes[metric]} disabled={availableScopes[metric].length === 1}>
|
<Input type="select" bind:value={selectedScopes[metric]} disabled={availableScopes[metric]?.length === 1}>
|
||||||
{#each (availableScopes[metric] || []) as scope}
|
{#each (availableScopes[metric] || []) as scope}
|
||||||
<option value={scope}>{scope}</option>
|
<option value={scope}>{scope}</option>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -106,7 +155,7 @@
|
|||||||
<th>Id</th>
|
<th>Id</th>
|
||||||
{/if}
|
{/if}
|
||||||
{#each ["min", "avg", "max"] as stat}
|
{#each ["min", "avg", "max"] as stat}
|
||||||
<th on:click={() => sortBy(metric, stat)}>
|
<th onclick={() => sortBy(metric, stat)}>
|
||||||
{stat}
|
{stat}
|
||||||
{#if selectedScopes[metric] == "node"}
|
{#if selectedScopes[metric] == "node"}
|
||||||
<Icon
|
<Icon
|
||||||
@@ -122,14 +171,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each hosts as host (host)}
|
{#each sortedHosts as host (host)}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{host}</th>
|
<th scope="col">{host}</th>
|
||||||
{#each selectedMetrics as metric (metric)}
|
{#each selectedMetrics as metric (metric)}
|
||||||
<StatsTableEntry
|
<StatsTableEntry
|
||||||
{data}
|
data={tableData[host][metric][selectedScopes[metric]]}
|
||||||
{host}
|
|
||||||
{metric}
|
|
||||||
scope={selectedScopes[metric]}
|
scope={selectedScopes[metric]}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -2,97 +2,83 @@
|
|||||||
@component Job-View subcomponent; Single Statistics entry component for statstable
|
@component Job-View subcomponent; Single Statistics entry component for statstable
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
- `host String`: The hostname (== node)
|
- `data [Object]`: The jobs statsdata for host-metric-scope
|
||||||
- `metric String`: The metric name
|
|
||||||
- `scope String`: The selected scope
|
- `scope String`: The selected scope
|
||||||
- `data [Object]`: The jobs statsdata
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@sveltestrap/sveltestrap";
|
import { Icon } from "@sveltestrap/sveltestrap";
|
||||||
|
|
||||||
export let host;
|
/* Svelte 5 Props */
|
||||||
export let metric;
|
let {
|
||||||
export let scope;
|
data,
|
||||||
export let data;
|
scope,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
let entrySorting = {
|
/* State Init */
|
||||||
id: { dir: "down", active: true },
|
let sortBy = $state("id");
|
||||||
min: { dir: "up", active: false },
|
let sortDir = $state("down");
|
||||||
avg: { dir: "up", active: false },
|
|
||||||
max: { dir: "up", active: false },
|
|
||||||
};
|
|
||||||
|
|
||||||
function compareNumbers(a, b) {
|
/* Derived */
|
||||||
return a.id - b.id;
|
const sortedData = $derived(updateData(data, sortBy, sortDir));
|
||||||
}
|
|
||||||
|
|
||||||
function sortByField(field) {
|
/* Functions */
|
||||||
let s = entrySorting[field];
|
function updateData(data, sortBy, sortDir) {
|
||||||
if (s.active) {
|
data.sort((a, b) => {
|
||||||
s.dir = s.dir == "up" ? "down" : "up";
|
if (a == null || b == null) {
|
||||||
} else {
|
return -1;
|
||||||
for (let field in entrySorting) entrySorting[field].active = false;
|
} else if (sortBy === "id") {
|
||||||
s.active = true;
|
return sortDir != "up"
|
||||||
}
|
? a[sortBy].localeCompare(b[sortBy], undefined, {numeric: true, sensitivity: 'base'})
|
||||||
|
: b[sortBy].localeCompare(a[sortBy], undefined, {numeric: true, sensitivity: 'base'});
|
||||||
entrySorting = { ...entrySorting };
|
|
||||||
stats = stats.sort((a, b) => {
|
|
||||||
if (a == null || b == null) return -1;
|
|
||||||
|
|
||||||
if (field === "id") {
|
|
||||||
return s.dir != "up" ?
|
|
||||||
a[field].localeCompare(b[field], undefined, {numeric: true, sensitivity: 'base'}) :
|
|
||||||
b[field].localeCompare(a[field], undefined, {numeric: true, sensitivity: 'base'})
|
|
||||||
} else {
|
} else {
|
||||||
return s.dir != "up"
|
return sortDir != "up"
|
||||||
? a.data[field] - b.data[field]
|
? a.data[sortBy] - b.data[sortBy]
|
||||||
: b.data[field] - a.data[field];
|
: b.data[sortBy] - a.data[sortBy];
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
return [...data];
|
||||||
|
};
|
||||||
$: stats = data
|
|
||||||
?.find((d) => d.name == metric && d.scope == scope)
|
|
||||||
?.stats.filter((s) => s.hostname == host && s.data != null)
|
|
||||||
?.sort(compareNumbers) || [];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if stats == null || stats.length == 0}
|
{#if data == null || data.length == 0}
|
||||||
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
|
<td colspan={scope == "node" ? 3 : 4}><i>No data</i></td>
|
||||||
{:else if stats.length == 1 && scope == "node"}
|
{:else if data.length == 1 && scope == "node"}
|
||||||
<td>
|
<td>
|
||||||
{stats[0].data.min}
|
{data[0].data.min}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{stats[0].data.avg}
|
{data[0].data.avg}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{stats[0].data.max}
|
{data[0].data.max}
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
<td colspan="4">
|
<td colspan="4">
|
||||||
<table style="width: 100%;">
|
<table style="width: 100%;">
|
||||||
<tr>
|
<tbody>
|
||||||
{#each ["id", "min", "avg", "max"] as field}
|
|
||||||
<th on:click={() => sortByField(field)}>
|
|
||||||
Sort
|
|
||||||
<Icon
|
|
||||||
name="caret-{entrySorting[field].dir}{entrySorting[field].active
|
|
||||||
? '-fill'
|
|
||||||
: ''}"
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{#each stats as s, i}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>{s.id ?? i}</th>
|
{#each ["id", "min", "avg", "max"] as field}
|
||||||
<td>{s.data.min}</td>
|
<th onclick={() => {
|
||||||
<td>{s.data.avg}</td>
|
sortBy = field;
|
||||||
<td>{s.data.max}</td>
|
sortDir = (sortDir == "up" ? "down" : "up");
|
||||||
|
}}>
|
||||||
|
Sort
|
||||||
|
<Icon
|
||||||
|
name="caret-{sortBy == field? sortDir: 'down'}{sortBy == field? '-fill': ''}"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{#each sortedData as s, i}
|
||||||
|
<tr>
|
||||||
|
<th>{s.id ?? i}</th>
|
||||||
|
<td>{s.data.min}</td>
|
||||||
|
<td>{s.data.avg}</td>
|
||||||
|
<td>{s.data.max}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import Jobs from './Jobs.root.svelte'
|
import Jobs from './Jobs.root.svelte'
|
||||||
|
|
||||||
new Jobs({
|
mount(Jobs, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
filterPresets: filterPresets,
|
filterPresets: filterPresets,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import List from './List.root.svelte'
|
import List from './List.root.svelte'
|
||||||
|
|
||||||
new List({
|
mount(List, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
filterPresets: filterPresets,
|
filterPresets: filterPresets,
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import Node from './Node.root.svelte'
|
import Node from './Node.root.svelte'
|
||||||
|
|
||||||
new Node({
|
mount(Node, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
cluster: infos.cluster,
|
cluster: infos.cluster,
|
||||||
hostname: infos.hostname,
|
hostname: infos.hostname,
|
||||||
from: infos.from,
|
presetFrom: infos.from,
|
||||||
to: infos.to
|
presetTo: infos.to
|
||||||
},
|
},
|
||||||
context: new Map([
|
context: new Map([
|
||||||
['cc-config', clusterCockpitConfig]
|
['cc-config', clusterCockpitConfig]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import Status from './Status.root.svelte'
|
import Status from './Status.root.svelte'
|
||||||
|
|
||||||
new Status({
|
mount(Status, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
cluster: infos.cluster,
|
cluster: infos.cluster,
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import Systems from './Systems.root.svelte'
|
import Systems from './Systems.root.svelte'
|
||||||
|
|
||||||
new Systems({
|
mount(Systems, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
displayType: displayType,
|
displayType: displayType,
|
||||||
cluster: infos.cluster,
|
cluster: infos.cluster,
|
||||||
subCluster: infos.subCluster,
|
subCluster: infos.subCluster,
|
||||||
from: infos.from,
|
fromPreset: infos.from,
|
||||||
to: infos.to
|
toPreset: infos.to
|
||||||
},
|
},
|
||||||
context: new Map([
|
context: new Map([
|
||||||
['cc-config', clusterCockpitConfig],
|
['cc-config', clusterCockpitConfig],
|
||||||
|
|||||||
@@ -10,38 +10,28 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte";
|
|
||||||
import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte";
|
import { queryStore, gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||||
import { Row, Col, Card, Table, Spinner } from "@sveltestrap/sveltestrap";
|
import { Row, Col, Card, Table, Spinner } from "@sveltestrap/sveltestrap";
|
||||||
import { stickyHeader } from "../generic/utils.js";
|
import { stickyHeader } from "../generic/utils.js";
|
||||||
import NodeListRow from "./nodelist/NodeListRow.svelte";
|
import NodeListRow from "./nodelist/NodeListRow.svelte";
|
||||||
import Pagination from "../generic/joblist/Pagination.svelte";
|
import Pagination from "../generic/joblist/Pagination.svelte";
|
||||||
|
|
||||||
export let cluster;
|
/* Svelte 5 Props */
|
||||||
export let subCluster = "";
|
let {
|
||||||
export let ccconfig = null;
|
cluster,
|
||||||
export let selectedMetrics = [];
|
subCluster = "",
|
||||||
export let selectedResolution = 0;
|
ccconfig = null,
|
||||||
export let hostnameFilter = "";
|
selectedMetrics = [],
|
||||||
export let systemUnits = null;
|
selectedResolution = 0,
|
||||||
export let from = null;
|
hostnameFilter = "",
|
||||||
export let to = null;
|
presetSystemUnits = null,
|
||||||
|
from = null,
|
||||||
|
to = null
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// Decouple from Job List Paging Params?
|
/* Const Init */
|
||||||
let usePaging = ccconfig?.node_list_usePaging || false
|
|
||||||
let itemsPerPage = usePaging ? (ccconfig?.plot_list_nodesPerPage || 10) : 10;
|
|
||||||
let page = 1;
|
|
||||||
let paging = { itemsPerPage, page };
|
|
||||||
|
|
||||||
let headerPaddingTop = 0;
|
|
||||||
stickyHeader(
|
|
||||||
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
|
|
||||||
(x) => (headerPaddingTop = x),
|
|
||||||
);
|
|
||||||
|
|
||||||
// const { query: initq } = init();
|
|
||||||
const initialized = getContext("initialized");
|
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
|
const usePaging = ccconfig?.node_list_usePaging || false;
|
||||||
const nodeListQuery = gql`
|
const nodeListQuery = gql`
|
||||||
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
|
query ($cluster: String!, $subCluster: String!, $nodeFilter: String!, $metrics: [String!], $scopes: [MetricScope!]!, $from: Time!, $to: Time!, $paging: PageRequest!, $selectedResolution: Int) {
|
||||||
nodeMetricsList(
|
nodeMetricsList(
|
||||||
@@ -91,51 +81,16 @@
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const updateConfigurationMutation = ({ name, value }) => {
|
/* State Init */
|
||||||
return mutationStore({
|
let nodes = $state([]);
|
||||||
client: client,
|
let page = $state(1);
|
||||||
query: gql`
|
let itemsPerPage = $state(usePaging ? (ccconfig?.plot_list_nodesPerPage || 10) : 10);
|
||||||
mutation ($name: String!, $value: String!) {
|
let headerPaddingTop = $state(0);
|
||||||
updateConfiguration(name: $name, value: $value)
|
let matchedNodes = $state(0);
|
||||||
}
|
|
||||||
`,
|
|
||||||
variables: { name, value },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Decouple from Job List Paging Params?
|
/* Derived */
|
||||||
function updateConfiguration(value, page) {
|
const paging = $derived({ itemsPerPage, page });
|
||||||
updateConfigurationMutation({
|
const nodesQuery = $derived(queryStore({
|
||||||
name: "plot_list_nodesPerPage",
|
|
||||||
value: value,
|
|
||||||
}).subscribe((res) => {
|
|
||||||
if (res.fetching === false && !res.error) {
|
|
||||||
nodes = [] // Empty List
|
|
||||||
paging = { itemsPerPage: value, page: page }; // Trigger reload of nodeList
|
|
||||||
} else if (res.fetching === false && res.error) {
|
|
||||||
throw res.error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usePaging) {
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
let {
|
|
||||||
scrollTop,
|
|
||||||
scrollHeight,
|
|
||||||
clientHeight
|
|
||||||
} = document.documentElement;
|
|
||||||
|
|
||||||
// Add 100 px offset to trigger load earlier
|
|
||||||
if (scrollTop + clientHeight >= scrollHeight - 100 && $nodesQuery?.data != null && $nodesQuery.data?.nodeMetricsList.hasNextPage) {
|
|
||||||
let pendingPaging = { ...paging }
|
|
||||||
pendingPaging.page += 1
|
|
||||||
paging = pendingPaging
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$: nodesQuery = queryStore({
|
|
||||||
client: client,
|
client: client,
|
||||||
query: nodeListQuery,
|
query: nodeListQuery,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -150,24 +105,93 @@
|
|||||||
selectedResolution: selectedResolution,
|
selectedResolution: selectedResolution,
|
||||||
},
|
},
|
||||||
requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change
|
requestPolicy: "network-only", // Resolution queries are cached, but how to access them? For now: reload on every change
|
||||||
|
}));
|
||||||
|
|
||||||
|
/* Effects */
|
||||||
|
$effect(() => {
|
||||||
|
if (!usePaging) {
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
let {
|
||||||
|
scrollTop,
|
||||||
|
scrollHeight,
|
||||||
|
clientHeight
|
||||||
|
} = document.documentElement;
|
||||||
|
|
||||||
|
// Add 100 px offset to trigger load earlier
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 100 && $nodesQuery?.data?.nodeMetricsList?.hasNextPage) {
|
||||||
|
page += 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
let nodes = [];
|
$effect(() => {
|
||||||
$: if ($initialized && $nodesQuery.data) {
|
handleNodes($nodesQuery?.data?.nodeMetricsList);
|
||||||
if (usePaging) {
|
});
|
||||||
nodes = [...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host));
|
|
||||||
} else {
|
$effect(() => {
|
||||||
nodes = nodes.concat([...$nodesQuery.data.nodeMetricsList.items].sort((a, b) => a.host.localeCompare(b.host)))
|
// Triggers (Except Paging)
|
||||||
|
from, to
|
||||||
|
selectedMetrics, selectedResolution
|
||||||
|
hostnameFilter
|
||||||
|
// Continous Scroll: Reset nodes and paging if parameters change: Existing entries will not match new selections
|
||||||
|
if (!usePaging) {
|
||||||
|
nodes = [];
|
||||||
|
page = 1;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Functions */
|
||||||
|
function handleNodes(data) {
|
||||||
|
if (data) {
|
||||||
|
matchedNodes = data.totalNodes;
|
||||||
|
if (usePaging || nodes.length == 0) {
|
||||||
|
nodes = [...data.items].sort((a, b) => a.host.localeCompare(b.host));
|
||||||
|
} else {
|
||||||
|
// Workaround to ignore secondary store triggers (reason tbd)
|
||||||
|
const oldNodes = $state.snapshot(nodes)
|
||||||
|
const newNodes = [...data.items].map((d) => d.host)
|
||||||
|
if (!oldNodes.some((n) => newNodes.includes(n.host))) {
|
||||||
|
nodes = nodes.concat([...data.items].sort((a, b) => a.host.localeCompare(b.host)))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decouple from Job List Paging Params?
|
||||||
|
function updateConfiguration(newItems, newPage) {
|
||||||
|
updateConfigurationMutation({
|
||||||
|
name: "plot_list_nodesPerPage",
|
||||||
|
value: newItems.toString(),
|
||||||
|
}).subscribe((res) => {
|
||||||
|
if (res.fetching === false && !res.error) {
|
||||||
|
nodes = []; // Empty List
|
||||||
|
itemsPerPage = newItems;
|
||||||
|
page = newPage; // Trigger reload of nodeList
|
||||||
|
} else if (res.fetching === false && res.error) {
|
||||||
|
throw res.error;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (!usePaging && (selectedMetrics || selectedResolution || hostnameFilter || from || to)) {
|
/* Const Functions */
|
||||||
// Continous Scroll: Reset list and paging if parameters change: Existing entries will not match new selections
|
const updateConfigurationMutation = ({ name, value }) => {
|
||||||
nodes = [];
|
return mutationStore({
|
||||||
paging = { itemsPerPage, page: 1 };
|
client: client,
|
||||||
}
|
query: gql`
|
||||||
|
mutation ($name: String!, $value: String!) {
|
||||||
|
updateConfiguration(name: $name, value: $value)
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: { name, value },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$: matchedNodes = $nodesQuery.data?.nodeMetricsList.totalNodes || matchedNodes;
|
/* Init Header */
|
||||||
|
stickyHeader(
|
||||||
|
".cc-table-wrapper > table.table >thead > tr > th.position-sticky:nth-child(1)",
|
||||||
|
(x) => (headerPaddingTop = x),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
@@ -192,7 +216,7 @@
|
|||||||
scope="col"
|
scope="col"
|
||||||
style="padding-top: {headerPaddingTop}px"
|
style="padding-top: {headerPaddingTop}px"
|
||||||
>
|
>
|
||||||
{metric} ({systemUnits[metric]})
|
{metric} ({presetSystemUnits[metric]})
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -212,7 +236,7 @@
|
|||||||
<td colspan={selectedMetrics.length + 1}> No nodes found </td>
|
<td colspan={selectedMetrics.length + 1}> No nodes found </td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{#if $nodesQuery.fetching || !$nodesQuery.data}
|
{#if $nodesQuery.fetching || !$nodesQuery.data}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan={selectedMetrics.length + 1}>
|
<td colspan={selectedMetrics.length + 1}>
|
||||||
@@ -238,16 +262,17 @@
|
|||||||
|
|
||||||
{#if usePaging}
|
{#if usePaging}
|
||||||
<Pagination
|
<Pagination
|
||||||
bind:page
|
{page}
|
||||||
{itemsPerPage}
|
{itemsPerPage}
|
||||||
itemText="Nodes"
|
itemText="Nodes"
|
||||||
totalItems={matchedNodes}
|
totalItems={matchedNodes}
|
||||||
on:update-paging={({ detail }) => {
|
updatePaging={(detail) => {
|
||||||
if (detail.itemsPerPage != itemsPerPage) {
|
if (detail.itemsPerPage != itemsPerPage) {
|
||||||
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
updateConfiguration(detail.itemsPerPage, detail.page);
|
||||||
} else {
|
} else {
|
||||||
nodes = []
|
nodes = [];
|
||||||
paging = { itemsPerPage: detail.itemsPerPage, page: detail.page };
|
itemsPerPage = detail.itemsPerPage;
|
||||||
|
page = detail.page;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,101 +10,99 @@
|
|||||||
<script>
|
<script>
|
||||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||||
import { Row, Col, Card, Spinner } from "@sveltestrap/sveltestrap";
|
import { Row, Col, Card, Spinner } from "@sveltestrap/sveltestrap";
|
||||||
import { init, checkMetricsDisabled } from "../generic/utils.js";
|
import { checkMetricDisabled } from "../generic/utils.js";
|
||||||
import MetricPlot from "../generic/plots/MetricPlot.svelte";
|
import MetricPlot from "../generic/plots/MetricPlot.svelte";
|
||||||
|
|
||||||
export let ccconfig = null;
|
/* Svelte 5 Props */
|
||||||
export let cluster = "";
|
let {
|
||||||
export const subCluster = "";
|
ccconfig = null,
|
||||||
export let selectedMetrics = null;
|
cluster = "",
|
||||||
export let hostnameFilter = "";
|
selectedMetric = "",
|
||||||
export let from = null;
|
hostnameFilter = "",
|
||||||
export let to = null;
|
from = null,
|
||||||
|
to = null
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const { query: initq } = init();
|
/* Const Init */
|
||||||
const client = getContextClient();
|
const client = getContextClient();
|
||||||
const nodeQuery = gql`
|
|
||||||
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
|
/* Derived */
|
||||||
nodeMetrics(
|
const nodesQuery = $derived(queryStore({
|
||||||
cluster: $cluster
|
client: client,
|
||||||
metrics: $metrics
|
query: gql`
|
||||||
from: $from
|
query ($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
|
||||||
to: $to
|
nodeMetrics(
|
||||||
) {
|
cluster: $cluster
|
||||||
host
|
metrics: $metrics
|
||||||
subCluster
|
from: $from
|
||||||
metrics {
|
to: $to
|
||||||
name
|
) {
|
||||||
scope
|
host
|
||||||
metric {
|
subCluster
|
||||||
timestep
|
metrics {
|
||||||
unit {
|
name
|
||||||
base
|
scope
|
||||||
prefix
|
metric {
|
||||||
}
|
timestep
|
||||||
series {
|
unit {
|
||||||
statistics {
|
base
|
||||||
min
|
prefix
|
||||||
avg
|
}
|
||||||
max
|
series {
|
||||||
|
statistics {
|
||||||
|
min
|
||||||
|
avg
|
||||||
|
max
|
||||||
|
}
|
||||||
|
data
|
||||||
}
|
}
|
||||||
data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
`,
|
||||||
`
|
|
||||||
|
|
||||||
$: selectedMetric = selectedMetrics[0] ? selectedMetrics[0] : "";
|
|
||||||
|
|
||||||
$: nodesQuery = queryStore({
|
|
||||||
client: client,
|
|
||||||
query: nodeQuery,
|
|
||||||
variables: {
|
variables: {
|
||||||
cluster: cluster,
|
cluster: cluster,
|
||||||
metrics: selectedMetrics,
|
metrics: [selectedMetric],
|
||||||
from: from.toISOString(),
|
from: from,
|
||||||
to: to.toISOString(),
|
to: to,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
let rawData = []
|
const mappedData = $derived(handleQueryData($nodesQuery?.data));
|
||||||
$: if ($initq.data && $nodesQuery?.data) {
|
const filteredData = $derived(mappedData.filter((h) => h.host.includes(hostnameFilter)));
|
||||||
rawData = $nodesQuery?.data?.nodeMetrics.filter((h) => {
|
|
||||||
if (h.subCluster === '') { // Exclude nodes with empty subCluster field
|
|
||||||
console.warn('subCluster not configured for node', h.host)
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return h.metrics.some(
|
|
||||||
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mappedData = []
|
/* Functions */
|
||||||
$: if (rawData?.length > 0) {
|
function handleQueryData(queryData) {
|
||||||
mappedData = rawData.map((h) => ({
|
let rawData = []
|
||||||
host: h.host,
|
if (queryData) {
|
||||||
subCluster: h.subCluster,
|
rawData = queryData.nodeMetrics.filter((h) => {
|
||||||
data: h.metrics.filter(
|
if (h.subCluster !== '') { // Exclude nodes with empty subCluster field
|
||||||
(m) => selectedMetrics.includes(m.name) && m.scope == "node",
|
return h.metrics.some(
|
||||||
),
|
(m) => m?.name == selectedMetric && m.scope == "node",
|
||||||
disabled: checkMetricsDisabled(
|
)
|
||||||
selectedMetrics,
|
};
|
||||||
cluster,
|
});
|
||||||
h.subCluster,
|
};
|
||||||
),
|
|
||||||
}))
|
let pendingMapped = [];
|
||||||
.sort((a, b) => a.host.localeCompare(b.host))
|
if (rawData.length > 0) {
|
||||||
}
|
pendingMapped = rawData.map((h) => ({
|
||||||
|
host: h.host,
|
||||||
let filteredData = []
|
subCluster: h.subCluster,
|
||||||
$: if (mappedData?.length > 0) {
|
data: h.metrics.filter(
|
||||||
filteredData = mappedData.filter((h) =>
|
(m) => m?.name == selectedMetric && m.scope == "node",
|
||||||
h.host.includes(hostnameFilter)
|
),
|
||||||
)
|
disabled: checkMetricDisabled(
|
||||||
|
selectedMetric,
|
||||||
|
cluster,
|
||||||
|
h.subCluster,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.host.localeCompare(b.host))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingMapped;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -132,22 +130,25 @@
|
|||||||
>{item.host} ({item.subCluster})</a
|
>{item.host} ({item.subCluster})</a
|
||||||
>
|
>
|
||||||
</h4>
|
</h4>
|
||||||
{#if item?.disabled[selectedMetric]}
|
{#if item?.disabled}
|
||||||
<Card body class="mx-3" color="info"
|
<Card body class="mx-3" color="info"
|
||||||
>Metric disabled for subcluster <code
|
>Metric disabled for subcluster <code
|
||||||
>{selectedMetric}:{item.subCluster}</code
|
>{selectedMetric}:{item.subCluster}</code
|
||||||
></Card
|
></Card
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- "No Data"-Warning included in MetricPlot-Component -->
|
<!-- "No Data"-Warning included in MetricPlot-Component -->
|
||||||
<MetricPlot
|
<!-- #key: X-axis keeps last selected timerange otherwise -->
|
||||||
timestep={item.data[0].metric.timestep}
|
{#key item.data[0].metric.series[0].data.length}
|
||||||
series={item.data[0].metric.series}
|
<MetricPlot
|
||||||
metric={item.data[0].name}
|
timestep={item.data[0].metric.timestep}
|
||||||
{cluster}
|
series={item.data[0].metric.series}
|
||||||
subCluster={item.subCluster}
|
metric={item.data[0].name}
|
||||||
forNode
|
{cluster}
|
||||||
/>
|
subCluster={item.subCluster}
|
||||||
|
forNode
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
height={175}
|
height={175}
|
||||||
forNode
|
forNode
|
||||||
/>
|
/>
|
||||||
<div class="my-2"/>
|
<div class="my-2"></div>
|
||||||
{#key extendedLegendData}
|
{#key extendedLegendData}
|
||||||
<MetricPlot
|
<MetricPlot
|
||||||
{cluster}
|
{cluster}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import Tags from './Tags.root.svelte'
|
import Tags from './Tags.root.svelte'
|
||||||
|
|
||||||
new Tags({
|
mount(Tags, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
username: username,
|
username: username,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
tagmap: tagmap,
|
presetTagmap: tagmap,
|
||||||
},
|
},
|
||||||
context: new Map([
|
context: new Map([
|
||||||
['cc-config', clusterCockpitConfig]
|
['cc-config', clusterCockpitConfig]
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
import {} from './header.entrypoint.js'
|
import {} from './header.entrypoint.js'
|
||||||
import User from './User.root.svelte'
|
import User from './User.root.svelte'
|
||||||
|
|
||||||
new User({
|
mount(User, {
|
||||||
target: document.getElementById('svelte-app'),
|
target: document.getElementById('svelte-app'),
|
||||||
props: {
|
props: {
|
||||||
filterPresets: filterPresets,
|
filterPresets: filterPresets,
|
||||||
|
|||||||
@@ -14,13 +14,12 @@
|
|||||||
{{block "stylesheets" .}}
|
{{block "stylesheets" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<script>
|
<script>
|
||||||
const header = {
|
// Used for header.entrypoint.js mount and filters/Resources.svelte
|
||||||
"username": "{{ .User.Username }}",
|
const hUsername = {{ .User.Username }};
|
||||||
"authlevel": {{ .User.GetAuthLevel }},
|
const hAuthlevel = {{ .User.GetAuthLevel }};
|
||||||
"clusters": {{ .Clusters }},
|
const hClusters = {{ .Clusters }};
|
||||||
"subClusters": {{ .SubClusters }},
|
const hSubClusters = {{ .SubClusters }};
|
||||||
"roles": {{ .Roles }}
|
const hRoles = {{ .Roles }};
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="site">
|
<body class="site">
|
||||||
|
|||||||
Reference in New Issue
Block a user