Skip to content

feat(canvas): gracefully filter excluded fields based on metrics security policy - #9549

Open
burnerlee wants to merge 1 commit into
rilldata:mainfrom
burnerlee:burnerlee/filter-canvas-components-by-metrics-policy
Open

burnerlee wants to merge 1 commit into
rilldata:mainfrom
burnerlee:burnerlee/filter-canvas-components-by-metrics-policy

Conversation

@burnerlee

@burnerlee burnerlee commented Jun 11, 2026

Copy link
Copy Markdown

When a metrics view security policy excludes a measure or dimension, canvas components now silently drop those fields instead of showing a ComponentError or a stuck loading/floating UI.

  • KPI Grid: excluded measures are filtered from the grid before rendering — only accessible KPIs are shown
  • Table (flat): inaccessible columns are silently dropped; only errors if no columns remain
  • Pivot: inaccessible measures/dimensions are filtered out; only errors if nothing usable remains
  • Leaderboard: same filtering approach as table
  • Pivot display now passes isLoading state to the schema validator to prevent a brief "Metrics view not found" error on initial load

Closes 8850

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

…rity 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 rilldata#8850
@burnerlee
burnerlee force-pushed the burnerlee/filter-canvas-components-by-metrics-policy branch from 247ba9d to 94ed2ee Compare June 11, 2026 17:10
@burnerlee
burnerlee marked this pull request as draft June 11, 2026 17:10
@burnerlee
burnerlee marked this pull request as ready for review June 11, 2026 18:41
@nishantmonu51 nishantmonu51 added Type:Feature New feature request Area:Dashboard Size:M Medium change: 100-499 lines labels Jun 30, 2026

@nishantmonu51 nishantmonu51 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch is three months behind main (merge base c9007517a0, June 11) and git merge-tree reports content conflicts in KPIGrid.svelte, leaderboard/selector.ts and CanvasPivotDisplay.svelte. Two landed changes matter for the rebase. #9674 already fixed the transient "Metrics view not found" flash by giving validateTableSchema/validateLeaderboardSchema a { metricsView, isLoading } argument and gating the pivotState update on !schema.isLoading, which makes the new isLoading parameter and the CanvasPivotRenderer schema type change redundant. More importantly, #9864 added adhoc_measures to the KPI grid, table, pivot and leaderboard specs, and those names are deliberately absent from metricsView.measures; the "keep only names present in metricsView.measures" filters in this PR would silently drop every ad hoc measure once rebased, so they need to union in the adhoc_measures names the way main's validators now do.

A second consequence of validating against validSpec: the frontend cannot distinguish "excluded by policy" from "does not exist", so a mistyped measure or dimension name in a table, pivot or leaderboard YAML now silently disappears in Rill Developer instead of producing the old Invalid measure(s) ... error. The parser does not validate component fields either, so nothing surfaces the typo any more.

Comment on lines +45 to +89
// Build accessible field lists by filtering out any fields not present in the
// metrics view spec (e.g. excluded by a security policy).
$: accessibleColumns =
"columns" in tableSpec
? (tableSpec.columns || []).filter((c) => {
const allMeasures =
metricsViewSpec?.measures?.map((m) => m.name as string) || [];
const allDimensions =
metricsViewSpec?.dimensions?.map(
(d) => d.name || (d.column as string),
) || [];
return allMeasures.includes(c) || allDimensions.includes(c);
})
: [];

$: accessibleMeasures =
!("columns" in tableSpec)
? (tableSpec.measures || []).filter((m) =>
metricsViewSpec?.measures?.some((mv) => mv.name === m),
)
: [];

$: accessibleRowDimensions =
!("columns" in tableSpec)
? (tableSpec.row_dimensions || []).filter(
(d) =>
metricsViewSpec?.dimensions?.some(
(mv) => mv.name === d || mv.column === d,
) ||
(metricsViewSpec?.timeDimension !== undefined &&
isTimeDimension(d, metricsViewSpec.timeDimension)),
)
: [];

$: accessibleColDimensions =
!("columns" in tableSpec)
? (tableSpec.col_dimensions || []).filter(
(d) =>
metricsViewSpec?.dimensions?.some(
(mv) => mv.name === d || mv.column === d,
) ||
(metricsViewSpec?.timeDimension !== undefined &&
isTimeDimension(d, metricsViewSpec.timeDimension)),
)
: [];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lists only reach pivotState, so they filter the header chips and nothing else — the query is still built from the raw spec. createPivotConfig -> processPivot reads $tableSpec.measures/row_dimensions/col_dimensions (dashboards/pivot/util.ts:196-206) and processFlat reads $tableSpec.columns (util.ts:277-296) directly, and createPivotDataStore builds the request from config.measureNames/rowDimensionNames/colDimensionNames (pivot-data-store.ts:219-250), not from config.pivot.columns. With a policy excluding revenue from measures: [revenue, orders], the chips show only orders while the aggregation request still contains revenue, the runtime rejects it at runtime/metricsview/ast.go:720-731 (measure "revenue" not found), and CanvasPivotRenderer shows PivotError — the error this PR is meant to remove. Flat tables fail differently but just as hard: processFlat treats any column not in the (already filtered) measure list as a dimension (util.ts:278), so the excluded measure is sent as a dimension and fails at ast.go:702-713. The filtering has to be applied where measureNames/rowDimensionNames/colDimensionNames are computed, or the config has to be derived from pivotState; a unit test asserting that an excluded field is absent from the query config rather than from the chips would pin this down.

Comment on lines +39 to +41
const accessibleColumns = (tableSpec?.columns || []).filter(
(c) => allMeasures.includes(c) || allDimensions.includes(c),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This drops legitimate time-grain columns. The table inspector explicitly allows "time" entries in columns (pivot/index.ts:211, allowedTypes: ["time", "dimension", "measure"]); those are named <timeDimension>_rill_TIME_GRAIN_* and are not in metricsView.dimensions, so they match neither allMeasures.includes nor allDimensions.includes. validatePivot below handles this with an isTimeDimension check, but neither validateFlat nor the matching accessibleColumns block in CanvasPivotDisplay.svelte has one. columns: [timestamp_rill_TIME_GRAIN_DAY, revenue] therefore loses its time column from pivotState while processFlat still queries it, and columns: [timestamp_rill_TIME_GRAIN_DAY] alone now fails with "Select at least one measure or dimension for the table".

Comment on lines +31 to +35
$: kpis = (kpiGridProperties.measures || [])
.filter(
(measure) =>
$metricsViewQuery.isLoading || accessibleMeasureNames.has(measure),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this filter empties kpis, the component renders its header over an empty grid rather than an error. validateKPIGridSchema only inspects the raw spec, so once $metricsViewQuery.isLoading is false and the metrics view is missing (query error, wrong name) or every configured measure is excluded, nothing renders and nothing explains why; previously each KPIProvider surfaced "Metrics view X not found" / "Invalid measure". Table, pivot and leaderboard all still error when nothing survives filtering, so the KPI grid is the odd one out.

Comment on lines +36 to +38
const measures = (leaderboardSpec?.measures || []).filter((m) =>
allMeasures.includes(m),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering measures here does not reach the sort measure, so the leaderboard still errors when the first configured measure is excluded. leaderboardState.leaderboardSortByMeasureName is initialised from the raw spec.measures[0] (leaderboard/index.ts:69, and again at :91 and :150 in validateAndResetSortMeasure), and LeaderboardDisplay falls back to the raw leaderboardMeasureNames?.[0] (LeaderboardDisplay.svelte:189-190); the aggregation query then sorts by a measure absent from visibleMeasures and the runtime rejects it in addOrderField with name not present in context (runtime/metricsview/ast.go:1039). This predates the PR, but the change here does not cover it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area:Dashboard Size:M Medium change: 100-499 lines Type:Feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants