Migrate user list and analysis view

This commit is contained in:
Christoph Kluge 2025-06-02 13:51:15 +02:00
parent 0b529a5c3c
commit 703556d893
4 changed files with 330 additions and 262 deletions

View File

@ -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,12 @@
}; };
} }
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 initialized = getContext("initialized");
const globalMetrics = getContext("globalMetrics"); 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 +69,22 @@
{ key: "project", label: "Project ID" }, { key: "project", label: "Project ID" },
]; ];
let sortSelection = /* Var Init */
let availableMetrics = [];
let metricUnits = {};
let metricScopes = {};
let rooflineMaxY;
let cluster;
let colWidth1, colWidth2;
let numBins = 50;
let maxY = -1;
/* 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 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,8 +92,9 @@
) || ) ||
sortOptions.find( sortOptions.find(
(option) => option.key == ccconfig.analysis_view_selectedTopCategory, (option) => option.key == ccconfig.analysis_view_selectedTopCategory,
)
); );
let groupSelection = let groupSelection = $state(
groupOptions.find( groupOptions.find(
(option) => (option) =>
option.key == option.key ==
@ -103,8 +102,10 @@
) || ) ||
groupOptions.find( groupOptions.find(
(option) => option.key == ccconfig.analysis_view_selectedTopEntity, (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,9 +122,14 @@
} }
}); });
const client = getContextClient(); /* Derived Vars */
let clusterName = $derived(cluster?.name ? cluster.name : cluster);
let metrics = $derived(
[...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()])]
);
$: statsQuery = queryStore({ let statsQuery = $derived(
queryStore({
client: client, client: client,
query: gql` query: gql`
query ($jobFilters: [JobFilter!]!) { query ($jobFilters: [JobFilter!]!) {
@ -146,9 +152,11 @@
} }
`, `,
variables: { jobFilters }, variables: { jobFilters },
}); })
);
$: topQuery = queryStore({ let topQuery = $derived(
queryStore({
client: client, client: client,
query: gql` query: gql`
query ( query (
@ -178,10 +186,12 @@
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(
queryStore({
client: client, client: client,
query: gql` query: gql`
query ($jobFilters: [JobFilter!]!, $metrics: [String!]!) { query ($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
@ -199,9 +209,11 @@
} }
`, `,
variables: { jobFilters, metrics }, variables: { jobFilters, metrics },
}); })
);
$: rooflineQuery = queryStore({ let rooflineQuery = $derived(
queryStore({
client: client, client: client,
query: gql` query: gql`
query ( query (
@ -233,8 +245,21 @@
maxX: 1000, maxX: 1000,
maxY, maxY,
}, },
})
);
/* Reactive Effects */
$effect(() => {
loadMetrics($initialized)
});
$effect(() => {
updateEntityConfiguration(groupSelection.key);
});
$effect(() => {
updateCategoryConfiguration(sortSelection.key);
}); });
/* Functions */
const updateConfigurationMutation = ({ name, value }) => { const updateConfigurationMutation = ({ name, value }) => {
return mutationStore({ return mutationStore({
client: client, client: client,
@ -287,9 +312,6 @@
} }
} }
let availableMetrics = [];
let metricUnits = {};
let metricScopes = {};
function loadMetrics(isInitialized) { function loadMetrics(isInitialized) {
if (!isInitialized) return if (!isInitialized) return
availableMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster.name))] availableMetrics = [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster.name))]
@ -299,10 +321,7 @@
} }
} }
$: loadMetrics($initialized) /* On Mount */
$: updateEntityConfiguration(groupSelection.key);
$: updateCategoryConfiguration(sortSelection.key);
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
</script> </script>
@ -329,7 +348,7 @@
{filterPresets} {filterPresets}
disableClusterSelection={true} disableClusterSelection={true}
startTimeQuickSelect={true} startTimeQuickSelect={true}
on:update-filters={({ detail }) => { applyFilters={(detail) => {
jobFilters = detail.filters; jobFilters = detail.filters;
}} }}
/> />

View File

@ -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,13 +49,19 @@
"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({
/* 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 jobFilters = $state([]);
let nameFilter = $state("");
let sorting = $state({ field: "totalJobs", direction: "down" });
/* Derived Vars */
let stats = $derived(
queryStore({
client: client, client: client,
query: gql` query: gql`
query($jobFilters: [JobFilter!]!) { query($jobFilters: [JobFilter!]!) {
@ -71,15 +75,12 @@
} }
}`, }`,
variables: { jobFilters }, variables: { jobFilters },
}); })
);
function changeSorting(event, field) { /* Functions */
let target = event.target; function changeSorting(field) {
while (target.tagName != "BUTTON") target = target.parentElement; sorting = { field, direction: sorting?.direction == "down" ? "up" : "down" };
let direction = target.children[0].className.includes("up") ? "down" : "up";
target.children[0].className = `bi-sort-numeric-${direction}`;
sorting = { field, direction };
} }
function sort(stats, sorting, nameFilter) { function sort(stats, sorting, nameFilter) {
@ -87,10 +88,10 @@
? (a, b) => b.id.localeCompare(a.id) ? (a, b) => b.id.localeCompare(a.id)
: (a, b) => a.id.localeCompare(b.id) : (a, b) => a.id.localeCompare(b.id)
// "-50": Forces empty strings to the end of the list // Force empty or undefined strings to the end of the list
const nameCmp = sorting.direction == "up" const nameCmp = sorting.direction == "up"
? (a, b) => (a.name == '') ? -50 : b.name.localeCompare(a.name) ? (a, b) => !a?.name ? 1 : (!b?.name ? -1 : (b.name.localeCompare(a.name)))
: (a, b) => (b.name == '') ? -50 : a.name.localeCompare(b.name) : (a, b) => !a?.name ? 1 : (!b?.name ? -1 : (a.name.localeCompare(b.name)))
const intCmp = sorting.direction == "up" const intCmp = sorting.direction == "up"
? (a, b) => a[sorting.field] - b[sorting.field] ? (a, b) => a[sorting.field] - b[sorting.field]
@ -105,6 +106,7 @@
} }
} }
/* On Mount */
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
</script> </script>
@ -129,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;
}} }}
/> />
@ -147,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"}
@ -158,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}
@ -169,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">
@ -179,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">
@ -189,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">
@ -199,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>

View File

@ -42,38 +42,55 @@
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"];
const metricBinOptions = [10, 20, 50, 100];
/* State Init */
// List & Control Vars
let filterComponent = $state(); // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
let jobFilters = $state([]);
let jobList = $state(null);
let matchedListJobs = $state(0);
let isSortingOpen = $state(false);
let isMetricsSelectionOpen = $state(false);
let sorting = $state({ field: "startTime", type: "col", order: "DESC" });
let selectedCluster = $state(filterPresets?.cluster ? filterPresets.cluster : null);
let metrics = $state(filterPresets.cluster
? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] ||
ccconfig.plot_list_selectedMetrics
: ccconfig.plot_list_selectedMetrics
);
let showFootprint = $state(filterPresets.cluster
? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
: !!ccconfig.plot_list_showFootprint
);
// 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
? ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || ( ccconfig['user_view_histogramMetrics'] || [] )
: ccconfig['user_view_histogramMetrics'] || []);
let stats = $derived(
queryStore({
client: client, client: client,
query: gql` query: gql`
query ($jobFilters: [JobFilter!]!, $selectedHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) { query ($jobFilters: [JobFilter!]!, $selectedHistograms: [String!], $numDurationBins: String, $numMetricBins: Int) {
@ -105,8 +122,10 @@
} }
`, `,
variables: { jobFilters, selectedHistograms, numDurationBins, numMetricBins }, variables: { jobFilters, selectedHistograms, numDurationBins, numMetricBins },
}); })
);
/* On Mount */
onMount(() => filterComponent.updateFilters()); onMount(() => filterComponent.updateFilters());
</script> </script>
@ -129,13 +148,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 +162,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 +192,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 +288,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>

View File

@ -103,6 +103,7 @@
setFilter({ cluster: pendingCluster, partition: pendingPartition }); setFilter({ cluster: pendingCluster, partition: pendingPartition });
}}>Close & Apply</Button }}>Close & Apply</Button
> >
{#if !disableClusterSelection}
<Button <Button
color="danger" color="danger"
onclick={() => { onclick={() => {
@ -112,6 +113,7 @@
setFilter({ cluster: pendingCluster, partition: pendingPartition}) setFilter({ cluster: pendingCluster, partition: pendingPartition})
}}>Reset</Button }}>Reset</Button
> >
{/if}
<Button onclick={() => (isOpen = false)}>Close</Button> <Button onclick={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>
</Modal> </Modal>