From d0580592bef96690d433ce330b337fadaa0d8123 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Fri, 17 Jan 2025 13:13:00 +0100 Subject: [PATCH] include feedback on nodeListView - display names of users and projects - stacked metricPlot for statsSeries --- internal/graph/schema.resolvers.go | 8 +- internal/metricDataDispatcher/dataLoader.go | 4 +- web/frontend/src/Header.svelte | 18 +- .../src/generic/plots/MetricPlot.svelte | 28 +-- .../src/systems/nodelist/NodeInfo.svelte | 227 ++++++++++-------- .../src/systems/nodelist/NodeListRow.svelte | 37 ++- 6 files changed, 188 insertions(+), 134 deletions(-) diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 9c877d1..20ad7cc 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -423,8 +423,8 @@ func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.Job // NodeMetrics is the resolver for the nodeMetrics field. func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) { user := repository.GetUserFromContext(ctx) - if user != nil && !user.HasRole(schema.RoleAdmin) { - return nil, errors.New("you need to be an administrator for this query") + if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { + return nil, errors.New("you need to be administrator or support staff for this query") } if metrics == nil { @@ -479,8 +479,8 @@ func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, sub } user := repository.GetUserFromContext(ctx) - if user != nil && !user.HasRole(schema.RoleAdmin) { - return nil, errors.New("you need to be an administrator for this query") + if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) { + return nil, errors.New("you need to be administrator or support staff for this query") } if metrics == nil { diff --git a/internal/metricDataDispatcher/dataLoader.go b/internal/metricDataDispatcher/dataLoader.go index 4642ffd..939a0fb 100644 --- a/internal/metricDataDispatcher/dataLoader.go +++ b/internal/metricDataDispatcher/dataLoader.go @@ -287,11 +287,11 @@ func LoadNodeListData( } // NOTE: New StatsSeries will always be calculated as 'min/median/max' - const maxSeriesSize int = 15 + const maxSeriesSize int = 8 for _, jd := range data { for _, scopes := range jd { for _, jm := range scopes { - if jm.StatisticsSeries != nil || len(jm.Series) <= maxSeriesSize { + if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize { continue } jm.AddStatisticsSeries() diff --git a/web/frontend/src/Header.svelte b/web/frontend/src/Header.svelte index 573a057..cf3e058 100644 --- a/web/frontend/src/Header.svelte +++ b/web/frontend/src/Header.svelte @@ -94,7 +94,7 @@ }, { title: "Nodes", - requiredRole: roles.admin, + requiredRole: roles.support, href: "/monitoring/systems/", icon: "hdd-rack", perCluster: true, @@ -102,19 +102,19 @@ menu: "Info", }, { - title: "Status", - requiredRole: roles.admin, - href: "/monitoring/status/", - icon: "clipboard-data", + title: "Analysis", + requiredRole: roles.support, + href: "/monitoring/analysis/", + icon: "graph-up", perCluster: true, listOptions: false, menu: "Info", }, { - title: "Analysis", - requiredRole: roles.support, - href: "/monitoring/analysis/", - icon: "graph-up", + title: "Status", + requiredRole: roles.admin, + href: "/monitoring/status/", + icon: "clipboard-data", perCluster: true, listOptions: false, menu: "Info", diff --git a/web/frontend/src/generic/plots/MetricPlot.svelte b/web/frontend/src/generic/plots/MetricPlot.svelte index f2bff02..3d143bd 100644 --- a/web/frontend/src/generic/plots/MetricPlot.svelte +++ b/web/frontend/src/generic/plots/MetricPlot.svelte @@ -9,7 +9,7 @@ - `height Number?`: The plot height [Default: 300] - `timestep Number`: The timestep used for X-axis rendering - `series [GraphQL.Series]`: The metric data object - - `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: null] + - `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: false] - `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null] - `cluster String`: Cluster name of the parent job / data - `subCluster String`: Name of the subCluster of the parent job @@ -128,7 +128,7 @@ export let height = 300; export let timestep; export let series; - export let useStatsSeries = null; + export let useStatsSeries = false; export let statisticsSeries = null; export let cluster = ""; export let subCluster; @@ -139,10 +139,9 @@ export let zoomState = null; export let thresholdState = null; - if (useStatsSeries == null) useStatsSeries = statisticsSeries != null; - if (useStatsSeries == false && series == null) useStatsSeries = true; + if (!useStatsSeries && statisticsSeries != null) useStatsSeries = true; - const usesMeanStatsSeries = (useStatsSeries?.mean && statisticsSeries.mean.length != 0) + const usesMeanStatsSeries = (statisticsSeries?.mean && statisticsSeries.mean.length != 0) const dispatch = createEventDispatcher(); const subClusterTopology = getContext("getHardwareTopology")(cluster, subCluster); const metricConfig = getContext("getMetricConfig")(cluster, subCluster, metric); @@ -205,11 +204,10 @@ // conditional hide series color markers: if ( - useStatsSeries === true || // Min/Max/Median Self-Explanatory + useStatsSeries || // Min/Max/Median Self-Explanatory dataSize === 1 || // Only one Y-Dataseries - dataSize > 6 + dataSize > 8 // More than 8 Y-Dataseries ) { - // More than 6 Y-Dataseries const idents = legendEl.querySelectorAll(".u-marker"); for (let i = 0; i < idents.length; i++) idents[i].style.display = "none"; @@ -240,7 +238,7 @@ "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)"; } - if (dataSize <= 12 || useStatsSeries === true) { + if (dataSize <= 12 || useStatsSeries) { return { hooks: { init: init, @@ -432,13 +430,13 @@ u.ctx.save(); u.ctx.textAlign = "start"; // 'end' u.ctx.fillStyle = "black"; - u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + 10); + u.ctx.fillText(textl, u.bbox.left + 10, u.bbox.top + (forNode ? 0 : 10)); u.ctx.textAlign = "end"; u.ctx.fillStyle = "black"; u.ctx.fillText( textr, u.bbox.left + u.bbox.width - 10, - u.bbox.top + 10, + u.bbox.top + (forNode ? 0 : 10), ); // u.ctx.fillText(text, u.bbox.left + u.bbox.width - 10, u.bbox.top + u.bbox.height - 10) // Recipe for bottom right @@ -496,10 +494,12 @@ }, legend: { // Display legend until max 12 Y-dataseries - show: series.length <= 12 || useStatsSeries === true ? true : false, - live: series.length <= 12 || useStatsSeries === true ? true : false, + show: series.length <= 12 || useStatsSeries, + live: series.length <= 12 || useStatsSeries, }, - cursor: { drag: { x: true, y: true } }, + cursor: { + drag: { x: true, y: true }, + } }; // RENDER HANDLING diff --git a/web/frontend/src/systems/nodelist/NodeInfo.svelte b/web/frontend/src/systems/nodelist/NodeInfo.svelte index 825bca7..057ef9f 100644 --- a/web/frontend/src/systems/nodelist/NodeInfo.svelte +++ b/web/frontend/src/systems/nodelist/NodeInfo.svelte @@ -45,6 +45,11 @@ $paging: PageRequest! ) { jobs(filter: $filter, order: $sorting, page: $paging) { + items { + user + project + exclusive + } count } } @@ -61,6 +66,13 @@ variables: { paging, sorting, filter }, }); + let userList; + let projectList; + $: if ($nodeJobsData?.data) { + userList = Array.from(new Set($nodeJobsData.data.jobs.items.map((j) => j.user))).sort((a, b) => a.localeCompare(b)); + projectList = Array.from(new Set($nodeJobsData.data.jobs.items.map((j) => j.project))).sort((a, b) => a.localeCompare(b)); + } + @@ -83,113 +95,124 @@ {#if $nodeJobsData.fetching} {:else if $nodeJobsData.data} -

- {#if healthWarn} - - - - - - Status - - - - {:else if metricWarn} - - - - - - Status - - - - {:else if $nodeJobsData.data.jobs.count > 0} - - - - - - Status - - - - {:else} - - - - - - Status - - - - {/if} -

-
-

- {#if $nodeJobsData.data.jobs.count > 0} - - - - - - Activity - - - - - List - - - {:else} - - - - - - Activity - - - - {/if} -

-

- + {#if healthWarn} + - + - - Show Users - - - - List - - -

-

- - + Status - - Show Projects - - - - List - + -

+ {:else if metricWarn} + + + + + + Status + + + + {:else if $nodeJobsData.data.jobs.count == 1 && $nodeJobsData.data.jobs.items[0].exclusive} + + + + + + Status + + + + {:else if $nodeJobsData.data.jobs.count >= 1 && !$nodeJobsData.data.jobs.items[0].exclusive} + + + + + + Status + + + + {:else} + + + + + + Status + + + + {/if} +
+ + + + + + + Activity + + + + + List + + + + + + + + + Users + + + + + List + + + {#if userList?.length > 0} + +
+ {userList.join(", ")} +
+
+ {/if} + + + + + + + Projects + + + + + List + + + {#if projectList?.length > 0} + +
+ {projectList.join(", ")} +
+
+ {/if} {/if}
diff --git a/web/frontend/src/systems/nodelist/NodeListRow.svelte b/web/frontend/src/systems/nodelist/NodeListRow.svelte index 1ea7d1d..817d953 100644 --- a/web/frontend/src/systems/nodelist/NodeListRow.svelte +++ b/web/frontend/src/systems/nodelist/NodeListRow.svelte @@ -46,13 +46,21 @@ return scopedNodeMetric; } }); + + let refinedData; + let dataHealth; + $: if (nodeData?.metrics) { + refinedData = sortAndSelectScope(nodeData?.metrics) + // Check data for series, skip disabled + dataHealth = refinedData.filter((rd) => rd.disabled === false).map((enabled) => (enabled.data.metric.series.length > 0)) + } - (m.metric.series.length > 0))}/> + - {#each sortAndSelectScope(nodeData?.metrics) as metricData (metricData.data.name)} + {#each refinedData as metricData (metricData.data.name)} {#if metricData?.disabled} {metricData.data.name}:{nodeData.subCluster} - {:else} + {:else if !!metricData.data?.metric.statisticsSeries} +
+ + {:else} + {/if}