From 94ed2ee5689a1e18de1f441cc077ceabac627126 Mon Sep 17 00:00:00 2001 From: Aviral Jain Date: Thu, 11 Jun 2026 22:31:19 +0530 Subject: [PATCH] feat(canvas): gracefully filter excluded fields based on metrics security policy When a metrics view security policy excludes a measure or dimension, canvas components now silently drop the excluded fields instead of showing a ComponentError or stuck loading state. - KPI Grid: filters out excluded measures before rendering; shows only the KPIs the current user can access - Table (flat): filters inaccessible columns; only errors if no columns remain - Pivot: filters inaccessible measures/dimensions; only errors if nothing usable remains - Leaderboard: filters inaccessible measures/dimensions consistently - Pivot display now passes loading state to validator to avoid a brief "Metrics view not found" flash on initial load Closes #8850 --- .../canvas/components/kpi-grid/KPIGrid.svelte | 35 +++++--- .../canvas/components/leaderboard/selector.ts | 37 ++------ .../pivot/CanvasPivotDisplay.svelte | 62 +++++++++++-- .../pivot/CanvasPivotRenderer.svelte | 1 + .../canvas/components/pivot/selector.ts | 90 +++++++------------ 5 files changed, 118 insertions(+), 107 deletions(-) diff --git a/web-common/src/features/canvas/components/kpi-grid/KPIGrid.svelte b/web-common/src/features/canvas/components/kpi-grid/KPIGrid.svelte index c2eee69328d0..07aa2313d6f5 100644 --- a/web-common/src/features/canvas/components/kpi-grid/KPIGrid.svelte +++ b/web-common/src/features/canvas/components/kpi-grid/KPIGrid.svelte @@ -13,22 +13,35 @@ $: ({ specStore, timeAndFilterStore, - parent: { name: canvasName }, + parent: { name: canvasName, metricsView }, visible, } = component); $: kpiGridProperties = $specStore; $: schema = validateKPIGridSchema(kpiGridProperties); - // Convert measures to KPI specs - $: kpis = (kpiGridProperties.measures || []).map((measure) => ({ - metrics_view: kpiGridProperties.metrics_view, - measure, - sparkline: kpiGridProperties.sparkline, - hide_time_range: kpiGridProperties.hide_time_range, - comparison: kpiGridProperties.comparison, - dimension_filters: kpiGridProperties.dimension_filters, - time_filters: kpiGridProperties.time_filters, - })); + $: metricsViewQuery = metricsView.getMetricsViewFromName( + kpiGridProperties.metrics_view, + ); + $: accessibleMeasureNames = new Set( + $metricsViewQuery.metricsView?.measures?.map((m) => m.name as string) ?? [], + ); + + // Convert measures to KPI specs, filtering out any excluded by a security + // policy (those will be absent from the metrics view's validSpec). + $: kpis = (kpiGridProperties.measures || []) + .filter( + (measure) => + $metricsViewQuery.isLoading || accessibleMeasureNames.has(measure), + ) + .map((measure) => ({ + metrics_view: kpiGridProperties.metrics_view, + measure, + sparkline: kpiGridProperties.sparkline, + hide_time_range: kpiGridProperties.hide_time_range, + comparison: kpiGridProperties.comparison, + dimension_filters: kpiGridProperties.dimension_filters, + time_filters: kpiGridProperties.time_filters, + })); $: filters = { time_filters: kpiGridProperties.time_filters, diff --git a/web-common/src/features/canvas/components/leaderboard/selector.ts b/web-common/src/features/canvas/components/leaderboard/selector.ts index e2666d1a1062..4c8ebecf7245 100644 --- a/web-common/src/features/canvas/components/leaderboard/selector.ts +++ b/web-common/src/features/canvas/components/leaderboard/selector.ts @@ -1,8 +1,4 @@ import type { LeaderboardSpec } from "@rilldata/web-common/features/canvas/components/leaderboard"; -import { - validateDimensions, - validateMeasures, -} from "@rilldata/web-common/features/canvas/components/validators"; import type { V1MetricsViewSpec } from "@rilldata/web-common/runtime-client"; export function validateLeaderboardSchema( @@ -35,8 +31,14 @@ export function validateLeaderboardSchema( const allDimensions = metricsView?.dimensions?.map((d) => d.name || (d.column as string)) || []; - let measures = leaderboardSpec?.measures || []; - let dimensions = leaderboardSpec?.dimensions || []; + // Filter to only accessible fields, silently dropping any excluded by a + // security policy. + const measures = (leaderboardSpec?.measures || []).filter((m) => + allMeasures.includes(m), + ); + const dimensions = (leaderboardSpec?.dimensions || []).filter((d) => + allDimensions.includes(d), + ); if (!measures.length || !dimensions.length) { return { @@ -45,29 +47,6 @@ export function validateLeaderboardSchema( }; } - measures = measures.filter((c) => allMeasures.includes(c)); - dimensions = dimensions.filter((c) => allDimensions.includes(c)); - - const validateMeasuresRes = validateMeasures(metricsView, measures); - if (!validateMeasuresRes.isValid) { - const invalidMeasures = validateMeasuresRes.invalidMeasures.join(", "); - return { - isValid: false, - error: `Invalid measure(s) "${invalidMeasures}" selected for the table`, - }; - } - - const validateDimensionsRes = validateDimensions(metricsView, dimensions); - - if (!validateDimensionsRes.isValid) { - const invalidDimensions = - validateDimensionsRes.invalidDimensions.join(", "); - - return { - isValid: false, - error: `Invalid dimension(s) "${invalidDimensions}" selected for the table`, - }; - } return { isValid: true, error: undefined, diff --git a/web-common/src/features/canvas/components/pivot/CanvasPivotDisplay.svelte b/web-common/src/features/canvas/components/pivot/CanvasPivotDisplay.svelte index 4b33042a9542..b27bce551863 100644 --- a/web-common/src/features/canvas/components/pivot/CanvasPivotDisplay.svelte +++ b/web-common/src/features/canvas/components/pivot/CanvasPivotDisplay.svelte @@ -1,5 +1,6 @@