Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/docs/reference/project-files/metrics-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,34 @@ _[object]_ - Defines an optional inline explore view for the metrics view. If no

- **`banner`** - _[string]_ - Custom banner displayed at the header of the explore view.

- **`dimensions`** - _[oneOf]_ - Dimensions to include in the explore view. Defaults to all dimensions in the metrics view.

- **option 1** - _[string]_ - Wildcard(*) selector that includes all available fields in the selection

- **option 2** - _[array of string]_ - Explicit list of fields to include in the selection

- **option 3** - _[object]_ - Advanced matching using regex, DuckDB expression, or exclusion

- **`regex`** - _[string]_ - Select fields using a regular expression

- **`expr`** - _[string]_ - DuckDB SQL expression to select fields based on custom logic

- **`exclude`** - _[object]_ - Select all fields except those listed here

- **`measures`** - _[oneOf]_ - Measures to include in the explore view. Defaults to all measures in the metrics view.

- **option 1** - _[string]_ - Wildcard(*) selector that includes all available fields in the selection

- **option 2** - _[array of string]_ - Explicit list of fields to include in the selection

- **option 3** - _[object]_ - Advanced matching using regex, DuckDB expression, or exclusion

- **`regex`** - _[string]_ - Select fields using a regular expression

- **`expr`** - _[string]_ - DuckDB SQL expression to select fields based on custom logic

- **`exclude`** - _[object]_ - Select all fields except those listed here

- **`theme`** - _[oneOf]_ - Name of the theme to use or define a theme inline. Either theme name or inline theme can be set.

- **option 1** - _[string]_ - Name of an existing theme to apply to the explore view.
Expand Down
12 changes: 9 additions & 3 deletions runtime/ai/instructions/data/resources/explore.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ Explore dashboards require minimal configuration. In most cases, you only need t

## Inline explores in metrics views

Metrics views create an explore resource by default with the same name as the metrics view. For legacy reasons, this does not happen for metrics views containing `version: 1`. You can customize a metrics view's explore with the `explore:` property inside the metrics view file:
The preferred way to create an explore is inline in the metrics view file: set `version: 1` and add an `explore:` block, which emits an explore resource with the same name as the metrics view (or `name:` if set):

```yaml
# metrics/sales.yaml
version: 1
type: metrics_view
display_name: Sales Analytics

Expand All @@ -45,8 +46,11 @@ measures:
- name: total_revenue
expression: SUM(revenue)

# Inline explore configuration (optional)
# Inline explore configuration
explore:
display_name: Sales Dashboard
dimensions: '*' # Optional: dimensions to expose ('*', a list, or {exclude: [...]}); defaults to all
measures: '*' # Optional: measures to expose ('*', a list, or {exclude: [...]}); defaults to all
time_ranges:
- P7D
- P30D
Expand All @@ -55,7 +59,9 @@ explore:
time_range: P30D
```

Use inline explores for simple cases where you want to keep the metrics view and its dashboard configuration together. Use separate explore files when you need multiple explores for the same metrics view or more complex configurations.
For legacy reasons, metrics views without `version:` auto-emit an explore even without an `explore:` block; metrics views with `version: 1` only emit one when the block is present.

Use inline explores to keep the metrics view and its dashboard configuration together. Use separate explore files when you need multiple explores for the same metrics view.

## Example with annotations

Expand Down
16 changes: 13 additions & 3 deletions runtime/ai/instructions/data/resources/metrics_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,28 +163,34 @@ format_d3: ",.0f" # 1,235 (rounded, with thousands separator)
- If there is any date/timestamp column in the underlying table, pick the primary or most interesting one and add it under `dimensions:`
- It is also _strongly_ recommended that you configure a primary time dimension using `timeseries:`

### Auto-generated explore
### Inline explore

When you create a metrics view, Rill automatically generates an explore dashboard with the same name, exposing all dimensions and measures. To customize the explore (you usually should not need to), add an `explore:` block:
New metrics views should set `version: 1` and include an `explore:` block, which makes Rill emit an explore dashboard for the metrics view (named after the metrics view unless `name:` is set):

```yaml
version: 1
explore:
display_name: Sales Dashboard
dimensions: '*' # Optional: dimensions to expose ('*', a list, or {exclude: [...]}); defaults to all
measures: '*' # Optional: measures to expose ('*', a list, or {exclude: [...]}); defaults to all
defaults:
time_range: P7D
measures:
- total_revenue
- order_count
```

**Legacy behavior**: Files with `version: 1` do NOT auto-generate an explore. Omit `version:` in new metrics views to get the auto-generated explore.
An empty block (`explore: {}`) is enough to enable the dashboard with all dimensions and measures. Note that `explore:` with no value (null) does NOT enable it. Set `explore: {skip: true}` to create a metrics view without a dashboard.

**Legacy behavior**: Files without `version:` (or `version: 0`) auto-generate an explore even without an `explore:` block. Files with `version: 1` only get an explore if an `explore:` block is present.

## Full Example

Here is a complete, annotated metrics view:

```yaml
# metrics/orders.yaml
version: 1
type: metrics_view

# Display metadata
Expand Down Expand Up @@ -255,6 +261,10 @@ measures:
expression: SUM(item_count) / NULLIF(COUNT(*), 0)
format_d3: ",.1f"
valid_percent_of_total: false

# Explore dashboard for this metrics view
explore:
display_name: Orders Dashboard
```

## Security Policies
Expand Down
20 changes: 18 additions & 2 deletions runtime/parser/parse_metrics_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ type MetricsViewYAML struct {
DisplayName string `yaml:"display_name"`
Description string `yaml:"description"`
Banner string `yaml:"banner"`
Dimensions *FieldSelectorYAML `yaml:"dimensions"`
Measures *FieldSelectorYAML `yaml:"measures"`
Theme yaml.Node `yaml:"theme"` // Name (string) or inline theme definition (map)
TimeRanges []ExploreTimeRangeYAML `yaml:"time_ranges"`
TimeZones []string `yaml:"time_zones"` // Single time zone or list of time zones
Expand Down Expand Up @@ -1128,6 +1130,18 @@ func (p *Parser) parseAndInsertInlineExplore(tmp *MetricsViewYAML, mvName string
allowCustomTimeRange = *tmp.Explore.AllowCustomTimeRange
}

// Resolve the dimensions and measures selectors; when omitted, they default to all fields.
var dimensionsSelector *runtimev1.FieldSelector
dimensions, ok := tmp.Explore.Dimensions.TryResolve()
if !ok {
dimensionsSelector = tmp.Explore.Dimensions.Proto()
}
var measuresSelector *runtimev1.FieldSelector
measures, ok := tmp.Explore.Measures.TryResolve()
if !ok {
measuresSelector = tmp.Explore.Measures.Proto()
}

refs := []ResourceName{{Kind: ResourceKindMetricsView, Name: mvName}}
// Parse theme if present.
// If it returns a themeSpec, it will be inserted as a separate resource later in this function.
Expand Down Expand Up @@ -1162,8 +1176,10 @@ func (p *Parser) parseAndInsertInlineExplore(tmp *MetricsViewYAML, mvName string
r.ExploreSpec.Description = tmp.Explore.Description
r.ExploreSpec.MetricsView = mvName
r.ExploreSpec.Banner = tmp.Explore.Banner
r.ExploreSpec.DimensionsSelector = &runtimev1.FieldSelector{Selector: &runtimev1.FieldSelector_All{All: true}}
r.ExploreSpec.MeasuresSelector = &runtimev1.FieldSelector{Selector: &runtimev1.FieldSelector_All{All: true}}
r.ExploreSpec.Dimensions = dimensions
r.ExploreSpec.DimensionsSelector = dimensionsSelector
r.ExploreSpec.Measures = measures
r.ExploreSpec.MeasuresSelector = measuresSelector
r.ExploreSpec.Theme = themeName
r.ExploreSpec.EmbeddedTheme = themeSpec
r.ExploreSpec.TimeRanges = timeRanges
Expand Down
81 changes: 81 additions & 0 deletions runtime/parser/parse_metrics_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1069,3 +1069,84 @@ rollups:
require.Len(t, mvSpec.Rollups, 1)
require.Equal(t, "-1Y to now", mvSpec.Rollups[0].DataTimeRange)
}

func TestMetricsViewInlineExploreFieldSelectors(t *testing.T) {
mvYAML := func(explore string) string {
return `
type: metrics_view
version: 1
model: m1
dimensions:
- name: foo
expression: id
measures:
- name: count
expression: COUNT(*)
` + explore
}

files := map[string]string{
`rill.yaml`: ``,
`models/m1.sql`: `SELECT 1 AS id`,
// No selectors: defaults to all dimensions and measures
`metrics_views/mv1.yaml`: mvYAML(`
explore: {}
`),
// Star and exclude selectors
`metrics_views/mv2.yaml`: mvYAML(`
explore:
dimensions: '*'
measures:
exclude: count
`),
// List and expression selectors
`metrics_views/mv3.yaml`: mvYAML(`
explore:
dimensions: [foo]
measures:
regex: 'count.*'
`),
// A null explore does not enable the inline explore
`metrics_views/mv4.yaml`: mvYAML(`
explore:
`),
// An explicitly skipped explore is not emitted either
`metrics_views/mv5.yaml`: mvYAML(`
explore:
skip: true
`),
}

ctx := context.Background()
repo := makeRepo(t, files)
p, err := Parse(ctx, repo, "", "", "duckdb", true)
require.NoError(t, err)
require.Empty(t, p.Errors)

explores := map[string]*runtimev1.ExploreSpec{}
for _, r := range p.Resources {
if r.Name.Kind == ResourceKindExplore {
explores[r.Name.Name] = r.ExploreSpec
}
}
require.Len(t, explores, 3)

e1 := explores["mv1"]
require.True(t, e1.DefinedInMetricsView)
require.Equal(t, "mv1", e1.MetricsView)
require.Empty(t, e1.Dimensions)
require.Equal(t, &runtimev1.FieldSelector{Selector: &runtimev1.FieldSelector_All{All: true}}, e1.DimensionsSelector)
require.Empty(t, e1.Measures)
require.Equal(t, &runtimev1.FieldSelector{Selector: &runtimev1.FieldSelector_All{All: true}}, e1.MeasuresSelector)

e2 := explores["mv2"]
require.True(t, e2.DefinedInMetricsView)
require.Equal(t, &runtimev1.FieldSelector{Selector: &runtimev1.FieldSelector_All{All: true}}, e2.DimensionsSelector)
require.Equal(t, &runtimev1.FieldSelector{Invert: true, Selector: &runtimev1.FieldSelector_Fields{Fields: &runtimev1.StringListValue{Values: []string{"count"}}}}, e2.MeasuresSelector)

e3 := explores["mv3"]
require.True(t, e3.DefinedInMetricsView)
require.Equal(t, []string{"foo"}, e3.Dimensions)
require.Nil(t, e3.DimensionsSelector)
require.Equal(t, &runtimev1.FieldSelector{Selector: &runtimev1.FieldSelector_Regex{Regex: "count.*"}}, e3.MeasuresSelector)
}
6 changes: 6 additions & 0 deletions runtime/parser/schema/project.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4117,6 +4117,12 @@ definitions:
banner:
type: string
description: Custom banner displayed at the header of the explore view.
dimensions:
$ref: '#/definitions/field_selector_properties'
description: Dimensions to include in the explore view. Defaults to all dimensions in the metrics view.
measures:
$ref: '#/definitions/field_selector_properties'
description: Measures to include in the explore view. Defaults to all measures in the metrics view.
theme:
oneOf:
- type: string
Expand Down
14 changes: 13 additions & 1 deletion runtime/server/generate_metrics_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ func (s *Server) generateMetricsViewYAMLWithAI(ctx context.Context, instanceID,
doc.Type = "metrics_view"
doc.TimeDimension = generateMetricsViewYAMLSimpleTimeDimension(tbl.Schema)
doc.Dimensions = generateMetricsViewYAMLSimpleDimensions(tbl.Schema)
if doc.DisplayName == "" {
doc.DisplayName = identifierToDisplayName(tbl.Name)
}
doc.Explore = &metricsViewExploreYAML{DisplayName: doc.DisplayName + " dashboard"}
for _, measure := range doc.Measures {
// Apply the default format preset to measures (the AI doesn't set the format preset).
measure.FormatPreset = "humanize"
Expand Down Expand Up @@ -425,6 +429,7 @@ func generateMetricsViewYAMLSimple(connector string, tbl *drivers.OlapTable, isM
TimeDimension: generateMetricsViewYAMLSimpleTimeDimension(tbl.Schema),
Dimensions: generateMetricsViewYAMLSimpleDimensions(tbl.Schema),
Measures: generateMetricsViewYAMLSimpleMeasures(tbl, dialect),
Explore: &metricsViewExploreYAML{DisplayName: identifierToDisplayName(tbl.Name) + " dashboard"},
}

doc.Connector = connector
Expand Down Expand Up @@ -525,6 +530,13 @@ type metricsViewYAML struct {
TimeDimension string `yaml:"timeseries,omitempty"`
Dimensions []*metricsViewDimensionYAML `yaml:"dimensions,omitempty"`
Measures []*metricsViewMeasureYAML `yaml:"measures,omitempty"`
Explore *metricsViewExploreYAML `yaml:"explore,omitempty"`
}

// metricsViewExploreYAML enables the inline explore dashboard for a generated metrics view.
// It must always be marshaled with at least one key: a null `explore:` value does not enable the explore.
type metricsViewExploreYAML struct {
DisplayName string `yaml:"display_name"`
}

type metricsViewDimensionYAML struct {
Expand Down Expand Up @@ -584,7 +596,7 @@ func insertEmptyLinesInYaml(node *yaml.Node) {
keyNode := node.Content[i].Content[j]
valueNode := node.Content[i].Content[j+1]

if keyNode.Value == "dimensions" || keyNode.Value == "measures" {
if keyNode.Value == "dimensions" || keyNode.Value == "measures" || keyNode.Value == "explore" {
keyNode.HeadComment = "\n"
}
if keyNode.Value == "type" {
Expand Down
11 changes: 10 additions & 1 deletion runtime/server/generate_metrics_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ driver: duckdb
"model: ad_bids",
"measures:",
"format_preset: humanize",
"explore:",
"display_name: Ad Bids dashboard",
},
},
{
Expand Down Expand Up @@ -108,6 +110,12 @@ driver: duckdb
}
})
}

// The generated metrics view should emit an inline explore.
testruntime.ReconcileParserAndWait(t, rt, instanceID)
explore := testruntime.GetResource(t, rt, instanceID, runtime.ResourceKindExplore, "generated_metrics_view")
require.Equal(t, "generated_metrics_view", explore.GetExplore().Spec.MetricsView)
require.True(t, explore.GetExplore().Spec.DefinedInMetricsView)
}

func TestGenerateMetricsViewWithAI(t *testing.T) {
Expand Down Expand Up @@ -171,8 +179,9 @@ func TestGenerateMetricsViewWithAI(t *testing.T) {
require.NoError(t, err)
require.True(t, res.AiSucceeded)

// Expecting 4 resources: the project parser, the model, the metrics view, and its inline explore.
testruntime.ReconcileParserAndWait(t, rt, instanceID)
testruntime.RequireReconcileState(t, rt, instanceID, 3, 0, 0)
testruntime.RequireReconcileState(t, rt, instanceID, 4, 0, 0)

data, err := repo.Get(ctx, "/metrics/generated_metrics_view.yaml")
require.NoError(t, err)
Expand Down
38 changes: 38 additions & 0 deletions web-common/src/features/dashboards/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,41 @@ export const useGetExploresForMetricsView = (
(res) => res.explore?.spec?.metricsView === metricsViewName,
);
};

// Canvases don't reference metrics views directly: their rows contain components,
// and each component's renderer properties name the metrics view it queries.
export const useGetCanvasesForMetricsView = (
client: RuntimeClient,
metricsViewName: string,
) => {
return createRuntimeServiceListResources(
client,
{},
{
query: {
select: (data) => {
const componentNames = new Set(
data.resources
?.filter(
(res) =>
res.meta?.name?.kind === ResourceKind.Component &&
res.component?.state?.validSpec?.rendererProperties
?.metrics_view === metricsViewName,
)
.map((res) => res.meta?.name?.name),
);
return (
data.resources?.filter(
(res) =>
res.meta?.name?.kind === ResourceKind.Canvas &&
res.canvas?.state?.validSpec?.rows?.some((row) =>
row.items?.some((item) => componentNames.has(item.component)),
),
) ?? []
);
},
},
},
queryClient,
);
};
5 changes: 4 additions & 1 deletion web-common/src/features/entity-management/add/new-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,13 @@ version: 1
type: metrics_view

model: # Choose a model to underpin your metrics view
timeseries: # Choose a timestamp column (if any) from your model
timeseries: # Choose a timestamp column (if any) from your model

dimensions:
measures:

explore:
display_name: # Optional: display name for this metrics view's explore dashboard
`;
case ResourceKind.Explore:
if (baseResource) {
Expand Down
Loading
Loading