feat: ephemeral measures UI for explore and canvas dashboards - #9864
feat: ephemeral measures UI for explore and canvas dashboards#9864nishantmonu51 wants to merge 10 commits into
Conversation
Frontend for ephemeral (calculated) measures on top of the `expression` measure compute: users define ad-hoc measures such as `profit = revenue - cost` from the explore measure selector, the pivot sidebar and the canvas inspector. Definitions live on the explore state (encoded in the `ephemeral` URL param and explore preset) or on a canvas component's `ephemeral_measures` prop, and every view attaches the expression compute to matching request measures. Claude-Session: https://claude.ai/code/session_01NW6ETnqzwL5jdoVCKQPHNf
The calc -> ephemeral rename also rewrote five CSS `calc(` tokens in MeasureBigNumber.svelte. The i18n guard flagged hardcoded strings in the ephemeral measure editors and in two files that now import messages, so those strings move to the catalogs and the format preset list is shared. Claude-Session: https://claude.ai/code/session_01NW6ETnqzwL5jdoVCKQPHNf
The YAML property on canvas components keeps the user-facing name `calculated_measures`; the runtime validator and tests follow. Code identifiers stay ephemeral-named. Claude-Session: https://claude.ai/code/session_01NW6ETnqzwL5jdoVCKQPHNf
User-facing labels, titles and URL-state errors now say "adhoc measure" instead of "calculated measure". Code identifiers and the canvas `calculated_measures` property are unchanged. Claude-Session: https://claude.ai/code/session_01NW6ETnqzwL5jdoVCKQPHNf
- Canvas chart validation accepts the chart's own ephemeral measures. - The active (sort) measure resolves ephemeral measures so single-measure leaderboards format values instead of rendering blanks. - The client expression parser uses the server's depth limit (32). - "All measures visible" counts ephemeral measures, so a hidden spec measure is never reported as visible to public URLs, chat context or the editor. - Public URLs keep pivot chips of ephemeral measures that survive sanitizing. - Editing a scheduled report recovers ephemeral definitions from the saved request instead of a non-existent explore store. - Removing the last visible measure re-seeds spec defaults. - Aliases derived from display names always pass name validation (shared slugify), and URL-state validation reserves every metrics view field. Claude-Session: https://claude.ai/code/session_01NW6ETnqzwL5jdoVCKQPHNf
Resolves conflicts with the unified expression filter refactor (#9746): adopts the new `where` construction and runes-mode components while keeping the ephemeral measure request mapping, and points the URL-state spec at the moved test helpers. Claude-Session: https://claude.ai/code/session_01NW6ETnqzwL5jdoVCKQPHNf
AdityaHegde
left a comment
There was a problem hiding this comment.
Measure used for QA issues in openrtb project,
name: spending_per_million_impressions
expr: overall_spend/ impressions * 1000
- Getting
effect_update_depth_exceededwhen sorting by computed measure in dimension display. Not a 100% sure what is triggering this, doesnt always happen. - Seeing
invalid measure \"spending_per_million_impressions\": measure \"spending_per_million_impressions\" not foundfor this measure's time series query error when comparing by dimension. - Report dialog's
Columnssection doesnt show displayName, shows the raw name instead. - Computed measure is not carried over to
Open in browserin report email. It is present in the csv download. - Alert seems to not recognise an ephemeral measure.
UX feedback,
- Do we need the measure in url even if it hidden? When user has a lot of such measures the url can unnecessarily get bloated. How about saving these in localStorage by metrics view? Any place this metrics view is referenced, user can add ephemeral measures to visible measure, including canvas. One big advantage is the measures are not lost if the user has not saved a bookmark.
- Should we show a warning when creating public url? If a dependant measure is hidden the computed measure gets hidden and the user wont know exactly why.
- Context columns like delta are not displayed for these measures. Feels a bit weird when timeseries has a line for comparison time range but no context column. I am guessing this is because we have
oneofforMeasureCompute? Is it planned to support both? - Do we need
nulls lastwhen sorting these? Or is it intended to shownullat the top when sorting in desc order? - In pivot you can add a measure from sidebar but only edit it if it is added to columns. Maybe we should add an edit in sidebar as well?
| class="truncate min-w-0 flex-1 text-left pointer-events-none text-fg-primary" | ||
| > | ||
| {displayName} | ||
| {#if ephemeralNames.has(item.id)} |
There was a problem hiding this comment.
Probably out of scope of this PR, but there seems to be a lot of duplication here if we need to add 5 instances for ephemeral editing.
Subcomponent is one option for shown vs hidden items. For tooltip maybe we can add a MaybeShowTooltip helper component.
There was a problem hiding this comment.
Extracted DashboardMetricsItemLabel, which owns the label, the ƒx marker and the edit button. The five call sites are now one component each and the file drops 51 lines.
| </div> | ||
|
|
||
| {#if ephemeralEditorOpen && component} | ||
| <EphemeralMeasureEditor |
There was a problem hiding this comment.
This also feels like a generic component like MultiFieldInput is responsible for a measure editor.
There was a problem hiding this comment.
The editor is mounted once at the inspector root (ComponentsEditor) and driven by canvasEphemeralMeasureEditor. The field inputs only set that store, so they no longer own an editor.
| @@ -0,0 +1,613 @@ | |||
| /** | |||
There was a problem hiding this comment.
How about reusing the filters parser? We use nearly and not hand write tokeniser and grammer. We could maintain the allow list only to match measure_expression.
Another option is to add an API to compile and get errors from backend.
There was a problem hiding this comment.
Replaced the hand-written tokeniser and recursive-descent parser with a nearley grammar (measure-expression.ne), built by build-measure-expression-grammar alongside the filter and rill-time grammars. The allow-list check against the metrics view stays in the parser wrapper.
- Rename the canvas YAML property to `adhoc_measures` so the user-facing name matches the "Adhoc measure" UI label. - Add an `additional-label` slot to SearchableFilterChip instead of the usage-specific `fx` prop. - Use `onCheckedChange` in SearchableMenuContent, replacing the deferred onclick workaround for the checkbox self-toggle race. - Render the menu's action slot in its own full-width bordered row rather than the select-all footer, fixing the hover in canvas field selectors, and share CreateEphemeralMeasureButton across explore and canvas. - Report dialog columns show ephemeral measures by display name. - Recover ephemeral definitions from a report's saved query so "Open in browser" restores the measure. - Alerts accept ephemeral measures; comparison and percent-of-total criteria stay disabled since those computes resolve against the spec. Claude-Session: https://claude.ai/code/session_011wsNGLshPEUqH6PePMnBkv
Comparing by dimension switches the default TDD chart to a Vega stacked bar, which builds its own query through the component chart provider. `createTDDCartesianSpec` never set `adhoc_measures`, so the ephemeral measure went out as a bare name and the runtime rejected it with `invalid measure "...": measure "..." not found`. Also adds a regression test asserting that every explore query naming an ephemeral measure carries its definition, across the time series, totals and dimension-comparison paths in both the explore and TDD views. Claude-Session: https://claude.ai/code/session_011wsNGLshPEUqH6PePMnBkv
|
On the reported bugs: 2. 3. Report dialog columns now resolve ephemeral measures to their display name, and they can be re-added as columns. 4. 5. Alerts accept ephemeral measures now — the saved query embeds the expression, so the name resolves without dashboard state. Comparison and percent-of-total criteria stay disabled for them, since 1. UX points 1-5 are unaddressed and still open for discussion. |
`timeoutID` was `$state`, and the effect that suppresses cell tooltips while scrolling both reads it (to clear the pending timeout) and writes it. Each write re-ran the effect, which wrote again, until Svelte aborted with `effect_update_depth_exceeded`. Nothing renders `timeoutID`, so a plain variable is enough. Reproduced in the browser by sorting a dimension table by an ephemeral measure: dropping its context columns resizes the table, which fires the scroll handler that sets `scrolling`. Claude-Session: https://claude.ai/code/session_011wsNGLshPEUqH6PePMnBkv
|
1.
Sorting by an ephemeral measure is what surfaced it: the measure has no context columns, so switching the sort to it resizes the table, which fires the scroll handler that sets Worth noting the loop was never specific to ephemeral measures — any scroll of the dimension table could hit it. |
UX feedback: - Persist ad-hoc measure definitions in a per-metrics-view localStorage library, so they survive without a bookmark and are shared by every explore on the same metrics view. The URL now carries only the definitions the explore state references, keeping shared links short. - Warn when creating a public URL that drops an ad-hoc measure because it references a measure the recipient cannot see. - Support comparison context columns (delta, delta percent, percent of total) for ad-hoc measures: a referenced measure may now resolve to an expression measure defined in the same query. - Sort ad-hoc measures with `NULLS LAST` in both dialects. - Allow editing and deleting an ad-hoc measure from the pivot sidebar, not just from the columns list. Code review: - Replace the hand-written tokeniser and recursive descent parser with a nearley grammar, matching the filter and rill-time parsers. - Move the canvas ad-hoc measure editor into a store mounted once at the inspector root, so the generic field inputs only open it. - Extract `DashboardMetricsItemLabel` to remove the duplicated label, marker and edit button markup in `DashboardMetricsDraggableList`. Claude-Session: https://claude.ai/code/session_01E6mxYCo1KqJpcxr1NMKPRA
|
On the UX feedback, addressed in f7576ee: 1. Definitions now live in a per-metrics-view localStorage library ( 2. Creating a public URL now lists the ad-hoc measures that will be dropped, with the hidden measures each one references. 3. Supported now, not a 4. Not intended. Both dialects now emit 5. Added edit and delete to the pivot sidebar. |
Frontend for ephemeral measures (labelled "Adhoc measures" in the UI) on top of the
expressionmeasure compute added in #9855: users define ad-hoc measures likeprofit = revenue - costin the UI, without editing the metrics view YAML.EphemeralMeasureDialog). Definitions live on the explore state and are honored by every view: big numbers, time series, leaderboards, dimension table, time dimension detail, pivot (including comparison chips, conditional formatting and exports).ephemeralURL param (name, display name, expression, optional format preset) and inExplorePreset.ephemeral_measures, so shared links, bookmarks and the most-recent-state loader reproduce them. Invalid or unknown definitions are dropped with a user-visible error, along with any columns or sorts that reference them.calculated_measuresrenderer prop, editable from the inspector (EphemeralMeasureEditor) and offered inline from the field selectors. Requests attach theexpressioncompute and labels/formatters resolve like for any spec measure.expression-parser.ts) mirrors the runtime allowlist (arithmetic, numeric literals, parentheses, unary minus,abs/round/floor/ceil/sqrt/ln/exp/power/coalesce/nullif/greatest/least) and gives inline errors. Names must not collide with metrics view fields or comparison accessor suffixes, and expressions can only reference plain measures (no window, required-dimension or time-comparison measures).EphemeralMeasureto theDashboardStateproto for the legacy proto-state path.Part of PLAT-135
Checklist: