From bcd80124a59c0aa5697bcfbad9f67b2026b4d987 Mon Sep 17 00:00:00 2001 From: Nishant Bangarwa Date: Sat, 20 Jun 2026 12:36:33 +0530 Subject: [PATCH 01/24] feat: canvas tab groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add inline tab groups to canvas dashboards. A top-level `rows:` entry is either a plain row or a `tabs:` group; a canvas can interleave free rows with multiple independent tab groups. Only the active tab's components mount, so inactive tabs issue no queries. Existing canvases render unchanged. - Proto/parser/reconciler: `CanvasTabGroup`/`CanvasTab` messages, parser validation (no items+tabs, no nesting, ≥1 tab, unique group/tab names), recursive component-name collection, schema + generated docs. - Frontend: `LayoutBlock`/`TabGroup` state, per-tab grids, prefixed component paths, query isolation via `{#if}`, per-group `?tabs=` URL state. - Visual editor: add/rename/delete/reorder tabs (menu + drag), add tab group from the add menu, convert rows to a tab group, cross-container component drag, delete-with-unwrap, in/outside add affordances, group boundary. --- .../project-files/canvas-dashboards.md | 10 +- proto/gen/rill/runtime/v1/resources.pb.go | 1038 ++++++++++------- .../rill/runtime/v1/resources.pb.validate.go | 302 +++++ .../gen/rill/runtime/v1/runtime.swagger.yaml | 38 +- proto/rill/runtime/v1/resources.proto | 23 +- runtime/canvases.go | 44 +- runtime/parser/parse_canvas.go | 312 +++-- runtime/parser/parser_test.go | 217 ++++ runtime/parser/schema/project.schema.yaml | 20 +- runtime/reconcilers/canvas.go | 8 +- .../canvas/AddComponentDropdown.svelte | 20 +- .../src/features/canvas/CanvasBuilder.svelte | 648 +++++++--- .../features/canvas/CanvasComponent.svelte | 2 + .../canvas/CanvasDashboardEmbed.svelte | 41 +- .../features/canvas/CanvasTabGroupView.svelte | 46 + .../src/features/canvas/CanvasTabStrip.svelte | 165 +++ .../features/canvas/EditableCanvasRow.svelte | 25 +- .../canvas/EditableCanvasTabGroup.svelte | 196 ++++ .../src/features/canvas/RowDropZone.svelte | 3 + .../features/canvas/StaticCanvasRow.svelte | 3 +- web-common/src/features/canvas/Toolbar.svelte | 11 +- .../canvas/components/charts/BaseChart.ts | 13 +- .../charts/custom-chart/chart-ai-agent.ts | 23 +- .../src/features/canvas/layout-util.spec.ts | 81 ++ web-common/src/features/canvas/layout-util.ts | 85 +- .../features/canvas/stores/canvas-entity.ts | 250 +++- .../features/canvas/stores/tab-edit.spec.ts | 273 +++++ .../src/features/canvas/stores/tab-edit.ts | 224 ++++ .../src/features/canvas/stores/tab-group.ts | 122 ++ .../proto/gen/rill/runtime/v1/resources_pb.ts | 115 +- .../src/runtime-client/gen/index.schemas.ts | 21 +- web-common/tests/web-admin-client.mock.ts | 6 + web-common/vite.config.ts | 6 + 33 files changed, 3607 insertions(+), 784 deletions(-) create mode 100644 web-common/src/features/canvas/CanvasTabGroupView.svelte create mode 100644 web-common/src/features/canvas/CanvasTabStrip.svelte create mode 100644 web-common/src/features/canvas/EditableCanvasTabGroup.svelte create mode 100644 web-common/src/features/canvas/layout-util.spec.ts create mode 100644 web-common/src/features/canvas/stores/tab-edit.spec.ts create mode 100644 web-common/src/features/canvas/stores/tab-edit.ts create mode 100644 web-common/src/features/canvas/stores/tab-group.ts create mode 100644 web-common/tests/web-admin-client.mock.ts diff --git a/docs/docs/reference/project-files/canvas-dashboards.md b/docs/docs/reference/project-files/canvas-dashboards.md index b7637aff9677..d3cb419aeff7 100644 --- a/docs/docs/reference/project-files/canvas-dashboards.md +++ b/docs/docs/reference/project-files/canvas-dashboards.md @@ -30,7 +30,7 @@ _[string]_ - Refers to the custom banner displayed at the header of an Canvas da ### `rows` -_[array of object]_ - Refers to all of the rows displayed on the Canvas +_[array of object]_ - Refers to all of the rows displayed on the Canvas. Each entry is either a plain row (with `items`) or a tab group (with `tabs`), but not both. - **`height`** - _[string]_ - Height of the row in px @@ -54,6 +54,14 @@ _[array of object]_ - Refers to all of the rows displayed on the Canvas - **`width`** - _[string, integer]_ - Width of the component (can be a number or string with unit) + - **`name`** - _[string]_ - Stable identifier for a tab group, used as its deep-link URL key. Defaults to `group-` if omitted. Only used for tab-group entries. + + - **`tabs`** - _[array of object]_ - Makes this entry a tab group instead of a plain row. Only the active tab's rows render; tabs cannot be nested. + + - **`label`** - _[string]_ - User-facing tab label. A URL-safe name is derived from it for deep-linking. + + - **`rows`** - _[array]_ - Plain rows (with `items`) shown when this tab is active. Tab rows cannot themselves contain `tabs`. + ### `max_width` _[integer]_ - Max width in pixels of the canvas diff --git a/proto/gen/rill/runtime/v1/resources.pb.go b/proto/gen/rill/runtime/v1/resources.pb.go index 126d63963e5c..6e6bfcf3b93b 100644 --- a/proto/gen/rill/runtime/v1/resources.pb.go +++ b/proto/gen/rill/runtime/v1/resources.pb.go @@ -5907,8 +5907,11 @@ type CanvasRow struct { Height *uint32 `protobuf:"varint,1,opt,name=height,proto3,oneof" json:"height,omitempty"` // Unit of the height. Current possible values: "px", empty string. HeightUnit string `protobuf:"bytes,2,opt,name=height_unit,json=heightUnit,proto3" json:"height_unit,omitempty"` - // Items to render in the row. + // Items to render in the row. Empty when the row is a tab group. Items []*CanvasItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"` + // If set, this row renders a tab group instead of items. + // A row has either items or a tab_group, never both. + TabGroup *CanvasTabGroup `protobuf:"bytes,4,opt,name=tab_group,json=tabGroup,proto3" json:"tab_group,omitempty"` } func (x *CanvasRow) Reset() { @@ -5964,6 +5967,138 @@ func (x *CanvasRow) GetItems() []*CanvasItem { return nil } +func (x *CanvasRow) GetTabGroup() *CanvasTabGroup { + if x != nil { + return x.TabGroup + } + return nil +} + +type CanvasTabGroup struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Stable identifier for the tab group, used for URL state. + // Defaults to "group-" if not provided in the canvas YAML. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Tabs in the group. A group always has at least one tab. + Tabs []*CanvasTab `protobuf:"bytes,2,rep,name=tabs,proto3" json:"tabs,omitempty"` +} + +func (x *CanvasTabGroup) Reset() { + *x = CanvasTabGroup{} + if protoimpl.UnsafeEnabled { + mi := &file_rill_runtime_v1_resources_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CanvasTabGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CanvasTabGroup) ProtoMessage() {} + +func (x *CanvasTabGroup) ProtoReflect() protoreflect.Message { + mi := &file_rill_runtime_v1_resources_proto_msgTypes[58] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CanvasTabGroup.ProtoReflect.Descriptor instead. +func (*CanvasTabGroup) Descriptor() ([]byte, []int) { + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{58} +} + +func (x *CanvasTabGroup) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CanvasTabGroup) GetTabs() []*CanvasTab { + if x != nil { + return x.Tabs + } + return nil +} + +type CanvasTab struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Stable identifier for the tab, used for URL state. Derived from the label. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // User-facing label for the tab. + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // Rows to render when the tab is active. These are always plain rows; + // a tab's rows never contain a nested tab_group. + Rows []*CanvasRow `protobuf:"bytes,3,rep,name=rows,proto3" json:"rows,omitempty"` +} + +func (x *CanvasTab) Reset() { + *x = CanvasTab{} + if protoimpl.UnsafeEnabled { + mi := &file_rill_runtime_v1_resources_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CanvasTab) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CanvasTab) ProtoMessage() {} + +func (x *CanvasTab) ProtoReflect() protoreflect.Message { + mi := &file_rill_runtime_v1_resources_proto_msgTypes[59] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CanvasTab.ProtoReflect.Descriptor instead. +func (*CanvasTab) Descriptor() ([]byte, []int) { + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{59} +} + +func (x *CanvasTab) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CanvasTab) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *CanvasTab) GetRows() []*CanvasRow { + if x != nil { + return x.Rows + } + return nil +} + type CanvasItem struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -5982,7 +6117,7 @@ type CanvasItem struct { func (x *CanvasItem) Reset() { *x = CanvasItem{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[58] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5995,7 +6130,7 @@ func (x *CanvasItem) String() string { func (*CanvasItem) ProtoMessage() {} func (x *CanvasItem) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[58] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[60] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6008,7 +6143,7 @@ func (x *CanvasItem) ProtoReflect() protoreflect.Message { // Deprecated: Use CanvasItem.ProtoReflect.Descriptor instead. func (*CanvasItem) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{58} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{60} } func (x *CanvasItem) GetComponent() string { @@ -6062,7 +6197,7 @@ type CanvasPreset struct { func (x *CanvasPreset) Reset() { *x = CanvasPreset{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[59] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6075,7 +6210,7 @@ func (x *CanvasPreset) String() string { func (*CanvasPreset) ProtoMessage() {} func (x *CanvasPreset) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[59] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[61] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6088,7 +6223,7 @@ func (x *CanvasPreset) ProtoReflect() protoreflect.Message { // Deprecated: Use CanvasPreset.ProtoReflect.Descriptor instead. func (*CanvasPreset) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{59} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{61} } func (x *CanvasPreset) GetTimeRange() string { @@ -6130,7 +6265,7 @@ type DefaultMetricsSQLFilter struct { func (x *DefaultMetricsSQLFilter) Reset() { *x = DefaultMetricsSQLFilter{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[60] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6143,7 +6278,7 @@ func (x *DefaultMetricsSQLFilter) String() string { func (*DefaultMetricsSQLFilter) ProtoMessage() {} func (x *DefaultMetricsSQLFilter) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[60] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[62] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6156,7 +6291,7 @@ func (x *DefaultMetricsSQLFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use DefaultMetricsSQLFilter.ProtoReflect.Descriptor instead. func (*DefaultMetricsSQLFilter) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{60} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{62} } func (x *DefaultMetricsSQLFilter) GetExpression() *Expression { @@ -6179,7 +6314,7 @@ type API struct { func (x *API) Reset() { *x = API{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[61] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6192,7 +6327,7 @@ func (x *API) String() string { func (*API) ProtoMessage() {} func (x *API) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[61] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[63] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6205,7 +6340,7 @@ func (x *API) ProtoReflect() protoreflect.Message { // Deprecated: Use API.ProtoReflect.Descriptor instead. func (*API) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{61} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{63} } func (x *API) GetSpec() *APISpec { @@ -6241,7 +6376,7 @@ type APISpec struct { func (x *APISpec) Reset() { *x = APISpec{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[62] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6254,7 +6389,7 @@ func (x *APISpec) String() string { func (*APISpec) ProtoMessage() {} func (x *APISpec) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[62] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[64] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6267,7 +6402,7 @@ func (x *APISpec) ProtoReflect() protoreflect.Message { // Deprecated: Use APISpec.ProtoReflect.Descriptor instead. func (*APISpec) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{62} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{64} } func (x *APISpec) GetResolver() string { @@ -6342,7 +6477,7 @@ type APIState struct { func (x *APIState) Reset() { *x = APIState{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[63] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6355,7 +6490,7 @@ func (x *APIState) String() string { func (*APIState) ProtoMessage() {} func (x *APIState) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[63] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[65] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6368,7 +6503,7 @@ func (x *APIState) ProtoReflect() protoreflect.Message { // Deprecated: Use APIState.ProtoReflect.Descriptor instead. func (*APIState) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{63} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{65} } type Schedule struct { @@ -6386,7 +6521,7 @@ type Schedule struct { func (x *Schedule) Reset() { *x = Schedule{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[64] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6399,7 +6534,7 @@ func (x *Schedule) String() string { func (*Schedule) ProtoMessage() {} func (x *Schedule) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[64] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[66] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6412,7 +6547,7 @@ func (x *Schedule) ProtoReflect() protoreflect.Message { // Deprecated: Use Schedule.ProtoReflect.Descriptor instead. func (*Schedule) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{64} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{66} } func (x *Schedule) GetRefUpdate() bool { @@ -6465,7 +6600,7 @@ type ParseError struct { func (x *ParseError) Reset() { *x = ParseError{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[65] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6478,7 +6613,7 @@ func (x *ParseError) String() string { func (*ParseError) ProtoMessage() {} func (x *ParseError) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[65] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[67] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6491,7 +6626,7 @@ func (x *ParseError) ProtoReflect() protoreflect.Message { // Deprecated: Use ParseError.ProtoReflect.Descriptor instead. func (*ParseError) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{65} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{67} } func (x *ParseError) GetMessage() string { @@ -6541,7 +6676,7 @@ type ValidationError struct { func (x *ValidationError) Reset() { *x = ValidationError{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[66] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6554,7 +6689,7 @@ func (x *ValidationError) String() string { func (*ValidationError) ProtoMessage() {} func (x *ValidationError) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[66] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[68] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6567,7 +6702,7 @@ func (x *ValidationError) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidationError.ProtoReflect.Descriptor instead. func (*ValidationError) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{66} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{68} } func (x *ValidationError) GetMessage() string { @@ -6596,7 +6731,7 @@ type DependencyError struct { func (x *DependencyError) Reset() { *x = DependencyError{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[67] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6609,7 +6744,7 @@ func (x *DependencyError) String() string { func (*DependencyError) ProtoMessage() {} func (x *DependencyError) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[67] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[69] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6622,7 +6757,7 @@ func (x *DependencyError) ProtoReflect() protoreflect.Message { // Deprecated: Use DependencyError.ProtoReflect.Descriptor instead. func (*DependencyError) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{67} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{69} } func (x *DependencyError) GetMessage() string { @@ -6650,7 +6785,7 @@ type ExecutionError struct { func (x *ExecutionError) Reset() { *x = ExecutionError{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[68] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6663,7 +6798,7 @@ func (x *ExecutionError) String() string { func (*ExecutionError) ProtoMessage() {} func (x *ExecutionError) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[68] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[70] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6676,7 +6811,7 @@ func (x *ExecutionError) ProtoReflect() protoreflect.Message { // Deprecated: Use ExecutionError.ProtoReflect.Descriptor instead. func (*ExecutionError) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{68} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{70} } func (x *ExecutionError) GetMessage() string { @@ -6697,7 +6832,7 @@ type CharLocation struct { func (x *CharLocation) Reset() { *x = CharLocation{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[69] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6710,7 +6845,7 @@ func (x *CharLocation) String() string { func (*CharLocation) ProtoMessage() {} func (x *CharLocation) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[69] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[71] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6723,7 +6858,7 @@ func (x *CharLocation) ProtoReflect() protoreflect.Message { // Deprecated: Use CharLocation.ProtoReflect.Descriptor instead. func (*CharLocation) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{69} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{71} } func (x *CharLocation) GetLine() uint32 { @@ -6745,7 +6880,7 @@ type ConnectorV2 struct { func (x *ConnectorV2) Reset() { *x = ConnectorV2{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[70] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6758,7 +6893,7 @@ func (x *ConnectorV2) String() string { func (*ConnectorV2) ProtoMessage() {} func (x *ConnectorV2) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[70] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[72] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6771,7 +6906,7 @@ func (x *ConnectorV2) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectorV2.ProtoReflect.Descriptor instead. func (*ConnectorV2) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{70} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{72} } func (x *ConnectorV2) GetSpec() *ConnectorSpec { @@ -6803,7 +6938,7 @@ type ConnectorSpec struct { func (x *ConnectorSpec) Reset() { *x = ConnectorSpec{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[71] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6816,7 +6951,7 @@ func (x *ConnectorSpec) String() string { func (*ConnectorSpec) ProtoMessage() {} func (x *ConnectorSpec) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[71] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[73] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6829,7 +6964,7 @@ func (x *ConnectorSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectorSpec.ProtoReflect.Descriptor instead. func (*ConnectorSpec) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{71} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{73} } func (x *ConnectorSpec) GetDriver() string { @@ -6878,7 +7013,7 @@ type ConnectorState struct { func (x *ConnectorState) Reset() { *x = ConnectorState{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[72] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6891,7 +7026,7 @@ func (x *ConnectorState) String() string { func (*ConnectorState) ProtoMessage() {} func (x *ConnectorState) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[72] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[74] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6904,7 +7039,7 @@ func (x *ConnectorState) ProtoReflect() protoreflect.Message { // Deprecated: Use ConnectorState.ProtoReflect.Descriptor instead. func (*ConnectorState) Descriptor() ([]byte, []int) { - return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{72} + return file_rill_runtime_v1_resources_proto_rawDescGZIP(), []int{74} } func (x *ConnectorState) GetSpecHash() string { @@ -6944,7 +7079,7 @@ type MetricsViewSpec_Dimension struct { func (x *MetricsViewSpec_Dimension) Reset() { *x = MetricsViewSpec_Dimension{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[73] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6957,7 +7092,7 @@ func (x *MetricsViewSpec_Dimension) String() string { func (*MetricsViewSpec_Dimension) ProtoMessage() {} func (x *MetricsViewSpec_Dimension) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[73] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[75] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7092,7 +7227,7 @@ type MetricsViewSpec_DimensionSelector struct { func (x *MetricsViewSpec_DimensionSelector) Reset() { *x = MetricsViewSpec_DimensionSelector{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[74] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7105,7 +7240,7 @@ func (x *MetricsViewSpec_DimensionSelector) String() string { func (*MetricsViewSpec_DimensionSelector) ProtoMessage() {} func (x *MetricsViewSpec_DimensionSelector) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[74] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[76] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7159,7 +7294,7 @@ type MetricsViewSpec_MeasureWindow struct { func (x *MetricsViewSpec_MeasureWindow) Reset() { *x = MetricsViewSpec_MeasureWindow{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[75] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7172,7 +7307,7 @@ func (x *MetricsViewSpec_MeasureWindow) String() string { func (*MetricsViewSpec_MeasureWindow) ProtoMessage() {} func (x *MetricsViewSpec_MeasureWindow) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[75] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[77] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7239,7 +7374,7 @@ type MetricsViewSpec_Measure struct { func (x *MetricsViewSpec_Measure) Reset() { *x = MetricsViewSpec_Measure{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[76] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7252,7 +7387,7 @@ func (x *MetricsViewSpec_Measure) String() string { func (*MetricsViewSpec_Measure) ProtoMessage() {} func (x *MetricsViewSpec_Measure) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[76] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[78] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7420,7 +7555,7 @@ type MetricsViewSpec_Annotation struct { func (x *MetricsViewSpec_Annotation) Reset() { *x = MetricsViewSpec_Annotation{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[77] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7433,7 +7568,7 @@ func (x *MetricsViewSpec_Annotation) String() string { func (*MetricsViewSpec_Annotation) ProtoMessage() {} func (x *MetricsViewSpec_Annotation) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[77] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[79] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7551,7 +7686,7 @@ type MetricsViewSpec_Rollup struct { func (x *MetricsViewSpec_Rollup) Reset() { *x = MetricsViewSpec_Rollup{} if protoimpl.UnsafeEnabled { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[78] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7564,7 +7699,7 @@ func (x *MetricsViewSpec_Rollup) String() string { func (*MetricsViewSpec_Rollup) ProtoMessage() {} func (x *MetricsViewSpec_Rollup) ProtoReflect() protoreflect.Message { - mi := &file_rill_runtime_v1_resources_proto_msgTypes[78] + mi := &file_rill_runtime_v1_resources_proto_msgTypes[80] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9093,7 +9228,7 @@ var file_rill_runtime_v1_resources_proto_rawDesc = []byte{ 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x65, 0x64, 0x5f, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0f, 0x64, - 0x61, 0x74, 0x61, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x65, 0x64, 0x4f, 0x6e, 0x22, 0x87, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x65, 0x64, 0x4f, 0x6e, 0x22, 0xc5, 0x01, 0x0a, 0x09, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x52, 0x6f, 0x77, 0x12, 0x1b, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x65, 0x69, @@ -9101,226 +9236,242 @@ var file_rill_runtime_v1_resources_proto_rawDesc = []byte{ 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x31, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x76, - 0x61, 0x73, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x42, 0x09, 0x0a, - 0x07, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x9a, 0x01, 0x0a, 0x0a, 0x43, 0x61, 0x6e, - 0x76, 0x61, 0x73, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f, - 0x6e, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, - 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, - 0x5f, 0x69, 0x6e, 0x5f, 0x63, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x49, 0x6e, 0x43, 0x61, 0x6e, 0x76, 0x61, - 0x73, 0x12, 0x19, 0x0a, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, - 0x48, 0x00, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, 0x68, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, - 0x77, 0x69, 0x64, 0x74, 0x68, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x77, 0x69, 0x64, 0x74, 0x68, 0x55, 0x6e, 0x69, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x5f, - 0x77, 0x69, 0x64, 0x74, 0x68, 0x22, 0x9c, 0x03, 0x0a, 0x0c, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, - 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x12, 0x22, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, - 0x61, 0x6e, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x4f, 0x0a, 0x0f, 0x63, 0x6f, - 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, - 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6d, - 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x0e, 0x63, 0x6f, 0x6d, - 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x14, 0x63, - 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x13, 0x63, 0x6f, 0x6d, - 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x44, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, - 0x88, 0x01, 0x01, 0x12, 0x4e, 0x0a, 0x0b, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x65, 0x78, - 0x70, 0x72, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, - 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x76, 0x61, - 0x73, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, 0x78, - 0x70, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, - 0x78, 0x70, 0x72, 0x1a, 0x67, 0x0a, 0x0f, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, 0x78, 0x70, - 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, - 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, - 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x53, 0x51, 0x4c, 0x46, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0d, 0x0a, 0x0b, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x17, 0x0a, 0x15, 0x5f, - 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x56, 0x0a, 0x17, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x53, 0x51, 0x4c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, - 0x3b, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, - 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x64, 0x0a, 0x03, - 0x41, 0x50, 0x49, 0x12, 0x2c, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x18, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, - 0x63, 0x12, 0x2f, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x22, 0xf8, 0x03, 0x0a, 0x07, 0x41, 0x50, 0x49, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1a, - 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x12, 0x48, 0x0a, 0x13, 0x72, 0x65, - 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x5f, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, - 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, - 0x74, 0x69, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x5f, - 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, - 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x36, 0x0a, - 0x17, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, - 0x65, 0x72, 0x73, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, - 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x1b, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, - 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, - 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x6f, 0x70, 0x65, 0x6e, - 0x61, 0x70, 0x69, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x3f, 0x0a, 0x1c, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x5f, - 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, - 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x6f, 0x70, 0x65, 0x6e, - 0x61, 0x70, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, - 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, - 0x5f, 0x64, 0x65, 0x66, 0x73, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x0b, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x11, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x44, 0x65, 0x66, 0x73, 0x50, - 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x44, 0x0a, 0x0e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, - 0x79, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x61, 0x73, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x3c, 0x0a, + 0x09, 0x74, 0x61, 0x62, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1f, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x54, 0x61, 0x62, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x52, 0x08, 0x74, 0x61, 0x62, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x09, 0x0a, 0x07, 0x5f, + 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x54, 0x0a, 0x0e, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, + 0x54, 0x61, 0x62, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x04, + 0x74, 0x61, 0x62, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x72, 0x69, 0x6c, + 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, + 0x76, 0x61, 0x73, 0x54, 0x61, 0x62, 0x52, 0x04, 0x74, 0x61, 0x62, 0x73, 0x22, 0x72, 0x0a, 0x09, + 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x54, 0x61, 0x62, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, + 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x2e, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x52, 0x6f, 0x77, 0x52, 0x04, 0x72, 0x6f, 0x77, 0x73, + 0x22, 0x9a, 0x01, 0x0a, 0x0a, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x49, 0x74, 0x65, 0x6d, 0x12, + 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x12, 0x2a, 0x0a, + 0x11, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x6e, 0x5f, 0x63, 0x61, 0x6e, 0x76, + 0x61, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x65, + 0x64, 0x49, 0x6e, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x12, 0x19, 0x0a, 0x05, 0x77, 0x69, 0x64, + 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x05, 0x77, 0x69, 0x64, 0x74, + 0x68, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x77, 0x69, 0x64, 0x74, 0x68, 0x5f, 0x75, 0x6e, + 0x69, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x77, 0x69, 0x64, 0x74, 0x68, 0x55, + 0x6e, 0x69, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x77, 0x69, 0x64, 0x74, 0x68, 0x22, 0x9c, 0x03, + 0x0a, 0x0c, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x12, 0x22, + 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x88, + 0x01, 0x01, 0x12, 0x4f, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, + 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x72, 0x69, + 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, + 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x4d, + 0x6f, 0x64, 0x65, 0x52, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, + 0x6e, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x01, 0x52, 0x13, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x44, + 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x4e, 0x0a, 0x0b, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2d, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6e, 0x76, 0x61, 0x73, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x2e, + 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, 0x78, 0x70, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, 0x78, 0x70, 0x72, 0x1a, 0x67, 0x0a, 0x0f, 0x46, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x45, 0x78, 0x70, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x3e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x28, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, + 0x53, 0x51, 0x4c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x72, 0x61, + 0x6e, 0x67, 0x65, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, + 0x6f, 0x6e, 0x5f, 0x64, 0x69, 0x6d, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x56, 0x0a, 0x17, + 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x53, 0x51, + 0x4c, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x72, 0x69, + 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x64, 0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, 0x2c, 0x0a, 0x04, 0x73, + 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x72, 0x69, 0x6c, 0x6c, + 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x53, + 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x2f, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, + 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x50, 0x49, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0xf8, 0x03, 0x0a, 0x07, 0x41, + 0x50, 0x49, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x72, 0x12, 0x48, 0x0a, 0x13, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x5f, 0x70, + 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x0f, + 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x53, 0x75, + 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x36, 0x0a, 0x17, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, + 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, + 0x1b, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x18, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x3f, 0x0a, 0x1c, + 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x5f, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x19, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, + 0x13, 0x6f, 0x70, 0x65, 0x6e, 0x61, 0x70, 0x69, 0x5f, 0x64, 0x65, 0x66, 0x73, 0x5f, 0x70, 0x72, + 0x65, 0x66, 0x69, 0x78, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6f, 0x70, 0x65, 0x6e, + 0x61, 0x70, 0x69, 0x44, 0x65, 0x66, 0x73, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x44, 0x0a, + 0x0e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, + 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, + 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, + 0x6c, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x6e, 0x65, 0x73, 0x74, + 0x65, 0x64, 0x5f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x12, 0x73, 0x6b, 0x69, 0x70, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x65, 0x63, + 0x75, 0x72, 0x69, 0x74, 0x79, 0x22, 0x0a, 0x0a, 0x08, 0x41, 0x50, 0x49, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x66, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x74, + 0x69, 0x63, 0x6b, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, + 0x64, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x7a, 0x6f, 0x6e, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x69, 0x6d, 0x65, 0x5a, 0x6f, 0x6e, 0x65, 0x22, + 0xbf, 0x01, 0x0a, 0x0a, 0x50, 0x61, 0x72, 0x73, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, + 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, + 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x44, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6c, + 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x73, 0x65, - 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x73, - 0x6b, 0x69, 0x70, 0x5f, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x73, 0x6b, 0x69, 0x70, 0x4e, - 0x65, 0x73, 0x74, 0x65, 0x64, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x22, 0x0a, 0x0a, - 0x08, 0x41, 0x50, 0x49, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x9b, 0x01, 0x0a, 0x08, 0x53, 0x63, - 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x5f, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x66, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x63, 0x72, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, - 0x72, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x72, 0x5f, 0x73, 0x65, - 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x74, 0x69, 0x63, - 0x6b, 0x65, 0x72, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x5f, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, - 0x69, 0x6d, 0x65, 0x5a, 0x6f, 0x6e, 0x65, 0x22, 0xbf, 0x01, 0x0a, 0x0a, 0x50, 0x61, 0x72, 0x73, - 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x43, 0x68, 0x61, 0x72, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, + 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, + 0x67, 0x22, 0x50, 0x0a, 0x0f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x23, + 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x50, + 0x61, 0x74, 0x68, 0x22, 0x4b, 0x0a, 0x0f, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, + 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x44, 0x0a, - 0x0e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, - 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x72, 0x4c, 0x6f, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, - 0x18, 0x0a, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0x50, 0x0a, 0x0f, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, - 0x74, 0x79, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x70, - 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x50, 0x61, 0x74, 0x68, 0x22, 0x4b, 0x0a, 0x0f, 0x44, - 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, - 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x65, - 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, - 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x22, 0x2a, 0x0a, 0x0e, 0x45, 0x78, 0x65, 0x63, - 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x22, 0x0a, 0x0c, 0x43, 0x68, 0x61, 0x72, 0x4c, 0x6f, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x65, 0x22, 0x78, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x56, 0x32, 0x12, 0x32, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, - 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x35, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x72, 0x69, 0x6c, - 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x22, 0xf1, 0x01, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x53, 0x70, 0x65, 0x63, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x0a, - 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, - 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x31, 0x0a, 0x14, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x13, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x72, - 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x70, 0x72, 0x6f, - 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x41, 0x72, 0x67, 0x73, 0x22, 0x2d, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x65, 0x63, - 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x70, 0x65, - 0x63, 0x48, 0x61, 0x73, 0x68, 0x2a, 0x8a, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, - 0x69, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x45, 0x43, - 0x4f, 0x4e, 0x43, 0x49, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x52, - 0x45, 0x43, 0x4f, 0x4e, 0x43, 0x49, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x49, 0x44, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x43, 0x4f, 0x4e, 0x43, - 0x49, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, - 0x4e, 0x47, 0x10, 0x02, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x43, 0x4f, 0x4e, 0x43, 0x49, 0x4c, - 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, - 0x10, 0x03, 0x2a, 0x8c, 0x01, 0x0a, 0x0f, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x21, 0x0a, 0x1d, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x5f, - 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, - 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x4d, 0x4f, 0x44, - 0x45, 0x4c, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, - 0x45, 0x53, 0x45, 0x54, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x5f, - 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x4d, 0x41, 0x4e, 0x55, - 0x41, 0x4c, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x5f, 0x43, 0x48, - 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x41, 0x54, 0x43, 0x48, 0x10, - 0x03, 0x2a, 0xab, 0x01, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6d, - 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x23, 0x45, - 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x41, 0x52, 0x49, 0x53, 0x4f, - 0x4e, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, - 0x43, 0x4f, 0x4d, 0x50, 0x41, 0x52, 0x49, 0x53, 0x4f, 0x4e, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, - 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x01, 0x12, 0x20, 0x0a, 0x1c, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, - 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x41, 0x52, 0x49, 0x53, 0x4f, 0x4e, 0x5f, 0x4d, 0x4f, 0x44, - 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x02, 0x12, 0x25, 0x0a, 0x21, 0x45, 0x58, 0x50, 0x4c, - 0x4f, 0x52, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x41, 0x52, 0x49, 0x53, 0x4f, 0x4e, 0x5f, 0x4d, - 0x4f, 0x44, 0x45, 0x5f, 0x44, 0x49, 0x4d, 0x45, 0x4e, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x2a, - 0xae, 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x57, 0x65, 0x62, 0x56, 0x69, - 0x65, 0x77, 0x12, 0x20, 0x0a, 0x1c, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x57, 0x45, - 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, - 0x57, 0x45, 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, - 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x57, 0x45, - 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x5f, 0x44, 0x49, 0x4d, 0x45, - 0x4e, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x45, 0x58, 0x50, 0x4c, 0x4f, - 0x52, 0x45, 0x5f, 0x57, 0x45, 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x50, 0x49, 0x56, 0x4f, - 0x54, 0x10, 0x03, 0x12, 0x1b, 0x0a, 0x17, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x57, - 0x45, 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x43, 0x41, 0x4e, 0x56, 0x41, 0x53, 0x10, 0x04, - 0x2a, 0xdc, 0x01, 0x0a, 0x0f, 0x45, 0x78, 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x53, 0x6f, 0x72, 0x74, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x1d, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, - 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x45, 0x58, 0x50, 0x4c, 0x4f, - 0x52, 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x56, 0x41, 0x4c, - 0x55, 0x45, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, - 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x45, 0x52, 0x43, 0x45, 0x4e, - 0x54, 0x10, 0x02, 0x12, 0x23, 0x0a, 0x1f, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x53, - 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x54, 0x41, 0x5f, 0x50, - 0x45, 0x52, 0x43, 0x45, 0x4e, 0x54, 0x10, 0x03, 0x12, 0x24, 0x0a, 0x20, 0x45, 0x58, 0x50, 0x4c, - 0x4f, 0x52, 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, - 0x4c, 0x54, 0x41, 0x5f, 0x41, 0x42, 0x53, 0x4f, 0x4c, 0x55, 0x54, 0x45, 0x10, 0x04, 0x12, 0x1f, - 0x0a, 0x1b, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x44, 0x49, 0x4d, 0x45, 0x4e, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x2a, - 0x85, 0x01, 0x0a, 0x0f, 0x41, 0x73, 0x73, 0x65, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x53, 0x53, 0x45, 0x52, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, - 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x53, 0x53, 0x45, 0x52, 0x54, 0x49, - 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x10, 0x01, - 0x12, 0x19, 0x0a, 0x15, 0x41, 0x53, 0x53, 0x45, 0x52, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, - 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x41, + 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, + 0x22, 0x2a, 0x0a, 0x0e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x22, 0x0a, 0x0c, + 0x43, 0x68, 0x61, 0x72, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, + 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x65, + 0x22, 0x78, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x56, 0x32, 0x12, + 0x32, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, + 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, + 0x70, 0x65, 0x63, 0x12, 0x35, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0xf1, 0x01, 0x0a, 0x0d, 0x43, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x70, 0x65, 0x63, 0x12, 0x16, 0x0a, 0x06, + 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x72, + 0x69, 0x76, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, + 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, + 0x74, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x12, 0x31, 0x0a, + 0x14, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x6f, 0x70, 0x65, + 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, + 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3e, + 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x72, 0x67, 0x73, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, + 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x41, 0x72, 0x67, 0x73, 0x22, 0x2d, + 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x70, 0x65, 0x63, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x70, 0x65, 0x63, 0x48, 0x61, 0x73, 0x68, 0x2a, 0x8a, 0x01, + 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x6e, 0x63, 0x69, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x45, 0x43, 0x4f, 0x4e, 0x43, 0x49, 0x4c, 0x45, 0x5f, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x52, 0x45, 0x43, 0x4f, 0x4e, 0x43, 0x49, 0x4c, 0x45, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x44, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x1c, + 0x0a, 0x18, 0x52, 0x45, 0x43, 0x4f, 0x4e, 0x43, 0x49, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x1c, 0x0a, 0x18, + 0x52, 0x45, 0x43, 0x4f, 0x4e, 0x43, 0x49, 0x4c, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x2a, 0x8c, 0x01, 0x0a, 0x0f, 0x4d, + 0x6f, 0x64, 0x65, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x21, + 0x0a, 0x1d, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x4d, + 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x47, + 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x45, 0x54, 0x10, 0x01, 0x12, 0x1c, + 0x0a, 0x18, 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x4d, + 0x4f, 0x44, 0x45, 0x5f, 0x4d, 0x41, 0x4e, 0x55, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, + 0x4d, 0x4f, 0x44, 0x45, 0x4c, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x4d, 0x4f, 0x44, + 0x45, 0x5f, 0x50, 0x41, 0x54, 0x43, 0x48, 0x10, 0x03, 0x2a, 0xab, 0x01, 0x0a, 0x15, 0x45, 0x78, + 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x61, 0x72, 0x69, 0x73, 0x6f, 0x6e, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x23, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x43, + 0x4f, 0x4d, 0x50, 0x41, 0x52, 0x49, 0x53, 0x4f, 0x4e, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, + 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x41, 0x52, 0x49, 0x53, + 0x4f, 0x4e, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x01, 0x12, 0x20, + 0x0a, 0x1c, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x41, 0x52, + 0x49, 0x53, 0x4f, 0x4e, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x10, 0x02, + 0x12, 0x25, 0x0a, 0x21, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, + 0x41, 0x52, 0x49, 0x53, 0x4f, 0x4e, 0x5f, 0x4d, 0x4f, 0x44, 0x45, 0x5f, 0x44, 0x49, 0x4d, 0x45, + 0x4e, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x2a, 0xae, 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6c, + 0x6f, 0x72, 0x65, 0x57, 0x65, 0x62, 0x56, 0x69, 0x65, 0x77, 0x12, 0x20, 0x0a, 0x1c, 0x45, 0x58, + 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x57, 0x45, 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, + 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x57, 0x45, 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, + 0x5f, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f, 0x45, 0x58, + 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x57, 0x45, 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, 0x5f, 0x54, + 0x49, 0x4d, 0x45, 0x5f, 0x44, 0x49, 0x4d, 0x45, 0x4e, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x12, + 0x1a, 0x0a, 0x16, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x57, 0x45, 0x42, 0x5f, 0x56, + 0x49, 0x45, 0x57, 0x5f, 0x50, 0x49, 0x56, 0x4f, 0x54, 0x10, 0x03, 0x12, 0x1b, 0x0a, 0x17, 0x45, + 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x57, 0x45, 0x42, 0x5f, 0x56, 0x49, 0x45, 0x57, 0x5f, + 0x43, 0x41, 0x4e, 0x56, 0x41, 0x53, 0x10, 0x04, 0x2a, 0xdc, 0x01, 0x0a, 0x0f, 0x45, 0x78, 0x70, + 0x6c, 0x6f, 0x72, 0x65, 0x53, 0x6f, 0x72, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x1d, + 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x1b, 0x0a, 0x17, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, + 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x50, 0x45, 0x52, 0x43, 0x45, 0x4e, 0x54, 0x10, 0x02, 0x12, 0x23, 0x0a, 0x1f, 0x45, + 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x44, 0x45, 0x4c, 0x54, 0x41, 0x5f, 0x50, 0x45, 0x52, 0x43, 0x45, 0x4e, 0x54, 0x10, 0x03, + 0x12, 0x24, 0x0a, 0x20, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x54, 0x41, 0x5f, 0x41, 0x42, 0x53, 0x4f, + 0x4c, 0x55, 0x54, 0x45, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, 0x45, 0x58, 0x50, 0x4c, 0x4f, 0x52, + 0x45, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x49, 0x4d, 0x45, + 0x4e, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x2a, 0x85, 0x01, 0x0a, 0x0f, 0x41, 0x73, 0x73, 0x65, + 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x53, 0x53, 0x45, 0x52, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x42, 0xc1, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, - 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x42, - 0x0e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, - 0x01, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x69, - 0x6c, 0x6c, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x72, 0x69, 0x6c, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x72, 0x69, 0x6c, 0x6c, 0x2f, 0x72, 0x75, 0x6e, 0x74, 0x69, - 0x6d, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x76, 0x31, 0xa2, - 0x02, 0x03, 0x52, 0x52, 0x58, 0xaa, 0x02, 0x0f, 0x52, 0x69, 0x6c, 0x6c, 0x2e, 0x52, 0x75, 0x6e, - 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0f, 0x52, 0x69, 0x6c, 0x6c, 0x5c, 0x52, - 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1b, 0x52, 0x69, 0x6c, 0x6c, - 0x5c, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x52, 0x69, 0x6c, 0x6c, 0x3a, 0x3a, - 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, + 0x15, 0x41, 0x53, 0x53, 0x45, 0x52, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x50, 0x41, 0x53, 0x53, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x41, 0x53, 0x53, 0x45, + 0x52, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, + 0x4c, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x41, 0x53, 0x53, 0x45, 0x52, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x42, + 0xc1, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x72, 0x69, 0x6c, 0x6c, 0x2e, 0x72, 0x75, 0x6e, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x69, 0x6c, 0x6c, 0x64, 0x61, 0x74, 0x61, 0x2f, 0x72, + 0x69, 0x6c, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x72, 0x69, + 0x6c, 0x6c, 0x2f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x72, 0x75, + 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x52, 0x52, 0x58, 0xaa, 0x02, 0x0f, + 0x52, 0x69, 0x6c, 0x6c, 0x2e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x56, 0x31, 0xca, + 0x02, 0x0f, 0x52, 0x69, 0x6c, 0x6c, 0x5c, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5c, 0x56, + 0x31, 0xe2, 0x02, 0x1b, 0x52, 0x69, 0x6c, 0x6c, 0x5c, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, + 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, + 0x02, 0x11, 0x52, 0x69, 0x6c, 0x6c, 0x3a, 0x3a, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x3a, + 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -9336,7 +9487,7 @@ func file_rill_runtime_v1_resources_proto_rawDescGZIP() []byte { } var file_rill_runtime_v1_resources_proto_enumTypes = make([]protoimpl.EnumInfo, 8) -var file_rill_runtime_v1_resources_proto_msgTypes = make([]protoimpl.MessageInfo, 84) +var file_rill_runtime_v1_resources_proto_msgTypes = make([]protoimpl.MessageInfo, 86) var file_rill_runtime_v1_resources_proto_goTypes = []any{ (ReconcileStatus)(0), // 0: rill.runtime.v1.ReconcileStatus (ModelChangeMode)(0), // 1: rill.runtime.v1.ModelChangeMode @@ -9404,41 +9555,43 @@ var file_rill_runtime_v1_resources_proto_goTypes = []any{ (*CanvasSpec)(nil), // 63: rill.runtime.v1.CanvasSpec (*CanvasState)(nil), // 64: rill.runtime.v1.CanvasState (*CanvasRow)(nil), // 65: rill.runtime.v1.CanvasRow - (*CanvasItem)(nil), // 66: rill.runtime.v1.CanvasItem - (*CanvasPreset)(nil), // 67: rill.runtime.v1.CanvasPreset - (*DefaultMetricsSQLFilter)(nil), // 68: rill.runtime.v1.DefaultMetricsSQLFilter - (*API)(nil), // 69: rill.runtime.v1.API - (*APISpec)(nil), // 70: rill.runtime.v1.APISpec - (*APIState)(nil), // 71: rill.runtime.v1.APIState - (*Schedule)(nil), // 72: rill.runtime.v1.Schedule - (*ParseError)(nil), // 73: rill.runtime.v1.ParseError - (*ValidationError)(nil), // 74: rill.runtime.v1.ValidationError - (*DependencyError)(nil), // 75: rill.runtime.v1.DependencyError - (*ExecutionError)(nil), // 76: rill.runtime.v1.ExecutionError - (*CharLocation)(nil), // 77: rill.runtime.v1.CharLocation - (*ConnectorV2)(nil), // 78: rill.runtime.v1.ConnectorV2 - (*ConnectorSpec)(nil), // 79: rill.runtime.v1.ConnectorSpec - (*ConnectorState)(nil), // 80: rill.runtime.v1.ConnectorState - (*MetricsViewSpec_Dimension)(nil), // 81: rill.runtime.v1.MetricsViewSpec.Dimension - (*MetricsViewSpec_DimensionSelector)(nil), // 82: rill.runtime.v1.MetricsViewSpec.DimensionSelector - (*MetricsViewSpec_MeasureWindow)(nil), // 83: rill.runtime.v1.MetricsViewSpec.MeasureWindow - (*MetricsViewSpec_Measure)(nil), // 84: rill.runtime.v1.MetricsViewSpec.Measure - (*MetricsViewSpec_Annotation)(nil), // 85: rill.runtime.v1.MetricsViewSpec.Annotation - (*MetricsViewSpec_Rollup)(nil), // 86: rill.runtime.v1.MetricsViewSpec.Rollup - nil, // 87: rill.runtime.v1.MetricsViewSpec.QueryAttributesEntry - nil, // 88: rill.runtime.v1.ReportSpec.AnnotationsEntry - nil, // 89: rill.runtime.v1.AlertSpec.AnnotationsEntry - nil, // 90: rill.runtime.v1.ThemeColors.VariablesEntry - nil, // 91: rill.runtime.v1.CanvasPreset.FilterExprEntry - (*timestamppb.Timestamp)(nil), // 92: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 93: google.protobuf.Struct - (*StructType)(nil), // 94: rill.runtime.v1.StructType - (TimeGrain)(0), // 95: rill.runtime.v1.TimeGrain - (*Expression)(nil), // 96: rill.runtime.v1.Expression - (ExportFormat)(0), // 97: rill.runtime.v1.ExportFormat - (*Color)(nil), // 98: rill.runtime.v1.Color - (*structpb.Value)(nil), // 99: google.protobuf.Value - (*Type)(nil), // 100: rill.runtime.v1.Type + (*CanvasTabGroup)(nil), // 66: rill.runtime.v1.CanvasTabGroup + (*CanvasTab)(nil), // 67: rill.runtime.v1.CanvasTab + (*CanvasItem)(nil), // 68: rill.runtime.v1.CanvasItem + (*CanvasPreset)(nil), // 69: rill.runtime.v1.CanvasPreset + (*DefaultMetricsSQLFilter)(nil), // 70: rill.runtime.v1.DefaultMetricsSQLFilter + (*API)(nil), // 71: rill.runtime.v1.API + (*APISpec)(nil), // 72: rill.runtime.v1.APISpec + (*APIState)(nil), // 73: rill.runtime.v1.APIState + (*Schedule)(nil), // 74: rill.runtime.v1.Schedule + (*ParseError)(nil), // 75: rill.runtime.v1.ParseError + (*ValidationError)(nil), // 76: rill.runtime.v1.ValidationError + (*DependencyError)(nil), // 77: rill.runtime.v1.DependencyError + (*ExecutionError)(nil), // 78: rill.runtime.v1.ExecutionError + (*CharLocation)(nil), // 79: rill.runtime.v1.CharLocation + (*ConnectorV2)(nil), // 80: rill.runtime.v1.ConnectorV2 + (*ConnectorSpec)(nil), // 81: rill.runtime.v1.ConnectorSpec + (*ConnectorState)(nil), // 82: rill.runtime.v1.ConnectorState + (*MetricsViewSpec_Dimension)(nil), // 83: rill.runtime.v1.MetricsViewSpec.Dimension + (*MetricsViewSpec_DimensionSelector)(nil), // 84: rill.runtime.v1.MetricsViewSpec.DimensionSelector + (*MetricsViewSpec_MeasureWindow)(nil), // 85: rill.runtime.v1.MetricsViewSpec.MeasureWindow + (*MetricsViewSpec_Measure)(nil), // 86: rill.runtime.v1.MetricsViewSpec.Measure + (*MetricsViewSpec_Annotation)(nil), // 87: rill.runtime.v1.MetricsViewSpec.Annotation + (*MetricsViewSpec_Rollup)(nil), // 88: rill.runtime.v1.MetricsViewSpec.Rollup + nil, // 89: rill.runtime.v1.MetricsViewSpec.QueryAttributesEntry + nil, // 90: rill.runtime.v1.ReportSpec.AnnotationsEntry + nil, // 91: rill.runtime.v1.AlertSpec.AnnotationsEntry + nil, // 92: rill.runtime.v1.ThemeColors.VariablesEntry + nil, // 93: rill.runtime.v1.CanvasPreset.FilterExprEntry + (*timestamppb.Timestamp)(nil), // 94: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 95: google.protobuf.Struct + (*StructType)(nil), // 96: rill.runtime.v1.StructType + (TimeGrain)(0), // 97: rill.runtime.v1.TimeGrain + (*Expression)(nil), // 98: rill.runtime.v1.Expression + (ExportFormat)(0), // 99: rill.runtime.v1.ExportFormat + (*Color)(nil), // 100: rill.runtime.v1.Color + (*structpb.Value)(nil), // 101: google.protobuf.Value + (*Type)(nil), // 102: rill.runtime.v1.Type } var file_rill_runtime_v1_resources_proto_depIdxs = []int32{ 9, // 0: rill.runtime.v1.Resource.meta:type_name -> rill.runtime.v1.ResourceMeta @@ -9454,53 +9607,53 @@ var file_rill_runtime_v1_resources_proto_depIdxs = []int32{ 54, // 10: rill.runtime.v1.Resource.theme:type_name -> rill.runtime.v1.Theme 58, // 11: rill.runtime.v1.Resource.component:type_name -> rill.runtime.v1.Component 62, // 12: rill.runtime.v1.Resource.canvas:type_name -> rill.runtime.v1.Canvas - 69, // 13: rill.runtime.v1.Resource.api:type_name -> rill.runtime.v1.API - 78, // 14: rill.runtime.v1.Resource.connector:type_name -> rill.runtime.v1.ConnectorV2 + 71, // 13: rill.runtime.v1.Resource.api:type_name -> rill.runtime.v1.API + 80, // 14: rill.runtime.v1.Resource.connector:type_name -> rill.runtime.v1.ConnectorV2 10, // 15: rill.runtime.v1.ResourceMeta.name:type_name -> rill.runtime.v1.ResourceName 10, // 16: rill.runtime.v1.ResourceMeta.refs:type_name -> rill.runtime.v1.ResourceName 10, // 17: rill.runtime.v1.ResourceMeta.owner:type_name -> rill.runtime.v1.ResourceName - 92, // 18: rill.runtime.v1.ResourceMeta.created_on:type_name -> google.protobuf.Timestamp - 92, // 19: rill.runtime.v1.ResourceMeta.spec_updated_on:type_name -> google.protobuf.Timestamp - 92, // 20: rill.runtime.v1.ResourceMeta.state_updated_on:type_name -> google.protobuf.Timestamp - 92, // 21: rill.runtime.v1.ResourceMeta.deleted_on:type_name -> google.protobuf.Timestamp + 94, // 18: rill.runtime.v1.ResourceMeta.created_on:type_name -> google.protobuf.Timestamp + 94, // 19: rill.runtime.v1.ResourceMeta.spec_updated_on:type_name -> google.protobuf.Timestamp + 94, // 20: rill.runtime.v1.ResourceMeta.state_updated_on:type_name -> google.protobuf.Timestamp + 94, // 21: rill.runtime.v1.ResourceMeta.deleted_on:type_name -> google.protobuf.Timestamp 0, // 22: rill.runtime.v1.ResourceMeta.reconcile_status:type_name -> rill.runtime.v1.ReconcileStatus - 92, // 23: rill.runtime.v1.ResourceMeta.reconcile_on:type_name -> google.protobuf.Timestamp + 94, // 23: rill.runtime.v1.ResourceMeta.reconcile_on:type_name -> google.protobuf.Timestamp 10, // 24: rill.runtime.v1.ResourceMeta.renamed_from:type_name -> rill.runtime.v1.ResourceName 12, // 25: rill.runtime.v1.ProjectParser.spec:type_name -> rill.runtime.v1.ProjectParserSpec 13, // 26: rill.runtime.v1.ProjectParser.state:type_name -> rill.runtime.v1.ProjectParserState - 73, // 27: rill.runtime.v1.ProjectParserState.parse_errors:type_name -> rill.runtime.v1.ParseError - 92, // 28: rill.runtime.v1.ProjectParserState.current_commit_on:type_name -> google.protobuf.Timestamp + 75, // 27: rill.runtime.v1.ProjectParserState.parse_errors:type_name -> rill.runtime.v1.ParseError + 94, // 28: rill.runtime.v1.ProjectParserState.current_commit_on:type_name -> google.protobuf.Timestamp 15, // 29: rill.runtime.v1.Source.spec:type_name -> rill.runtime.v1.SourceSpec 16, // 30: rill.runtime.v1.Source.state:type_name -> rill.runtime.v1.SourceState - 93, // 31: rill.runtime.v1.SourceSpec.properties:type_name -> google.protobuf.Struct - 72, // 32: rill.runtime.v1.SourceSpec.refresh_schedule:type_name -> rill.runtime.v1.Schedule - 92, // 33: rill.runtime.v1.SourceState.refreshed_on:type_name -> google.protobuf.Timestamp + 95, // 31: rill.runtime.v1.SourceSpec.properties:type_name -> google.protobuf.Struct + 74, // 32: rill.runtime.v1.SourceSpec.refresh_schedule:type_name -> rill.runtime.v1.Schedule + 94, // 33: rill.runtime.v1.SourceState.refreshed_on:type_name -> google.protobuf.Timestamp 18, // 34: rill.runtime.v1.Model.spec:type_name -> rill.runtime.v1.ModelSpec 19, // 35: rill.runtime.v1.Model.state:type_name -> rill.runtime.v1.ModelState - 72, // 36: rill.runtime.v1.ModelSpec.refresh_schedule:type_name -> rill.runtime.v1.Schedule - 93, // 37: rill.runtime.v1.ModelSpec.incremental_state_resolver_properties:type_name -> google.protobuf.Struct - 93, // 38: rill.runtime.v1.ModelSpec.partitions_resolver_properties:type_name -> google.protobuf.Struct - 93, // 39: rill.runtime.v1.ModelSpec.input_properties:type_name -> google.protobuf.Struct - 93, // 40: rill.runtime.v1.ModelSpec.stage_properties:type_name -> google.protobuf.Struct - 93, // 41: rill.runtime.v1.ModelSpec.output_properties:type_name -> google.protobuf.Struct + 74, // 36: rill.runtime.v1.ModelSpec.refresh_schedule:type_name -> rill.runtime.v1.Schedule + 95, // 37: rill.runtime.v1.ModelSpec.incremental_state_resolver_properties:type_name -> google.protobuf.Struct + 95, // 38: rill.runtime.v1.ModelSpec.partitions_resolver_properties:type_name -> google.protobuf.Struct + 95, // 39: rill.runtime.v1.ModelSpec.input_properties:type_name -> google.protobuf.Struct + 95, // 40: rill.runtime.v1.ModelSpec.stage_properties:type_name -> google.protobuf.Struct + 95, // 41: rill.runtime.v1.ModelSpec.output_properties:type_name -> google.protobuf.Struct 1, // 42: rill.runtime.v1.ModelSpec.change_mode:type_name -> rill.runtime.v1.ModelChangeMode 20, // 43: rill.runtime.v1.ModelSpec.tests:type_name -> rill.runtime.v1.ModelTest - 93, // 44: rill.runtime.v1.ModelState.result_properties:type_name -> google.protobuf.Struct - 92, // 45: rill.runtime.v1.ModelState.refreshed_on:type_name -> google.protobuf.Timestamp - 93, // 46: rill.runtime.v1.ModelState.incremental_state:type_name -> google.protobuf.Struct - 94, // 47: rill.runtime.v1.ModelState.incremental_state_schema:type_name -> rill.runtime.v1.StructType - 93, // 48: rill.runtime.v1.ModelTest.resolver_properties:type_name -> google.protobuf.Struct + 95, // 44: rill.runtime.v1.ModelState.result_properties:type_name -> google.protobuf.Struct + 94, // 45: rill.runtime.v1.ModelState.refreshed_on:type_name -> google.protobuf.Timestamp + 95, // 46: rill.runtime.v1.ModelState.incremental_state:type_name -> google.protobuf.Struct + 96, // 47: rill.runtime.v1.ModelState.incremental_state_schema:type_name -> rill.runtime.v1.StructType + 95, // 48: rill.runtime.v1.ModelTest.resolver_properties:type_name -> google.protobuf.Struct 22, // 49: rill.runtime.v1.MetricsView.spec:type_name -> rill.runtime.v1.MetricsViewSpec 28, // 50: rill.runtime.v1.MetricsView.state:type_name -> rill.runtime.v1.MetricsViewState - 95, // 51: rill.runtime.v1.MetricsViewSpec.smallest_time_grain:type_name -> rill.runtime.v1.TimeGrain - 81, // 52: rill.runtime.v1.MetricsViewSpec.dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.Dimension - 84, // 53: rill.runtime.v1.MetricsViewSpec.measures:type_name -> rill.runtime.v1.MetricsViewSpec.Measure + 97, // 51: rill.runtime.v1.MetricsViewSpec.smallest_time_grain:type_name -> rill.runtime.v1.TimeGrain + 83, // 52: rill.runtime.v1.MetricsViewSpec.dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.Dimension + 86, // 53: rill.runtime.v1.MetricsViewSpec.measures:type_name -> rill.runtime.v1.MetricsViewSpec.Measure 35, // 54: rill.runtime.v1.MetricsViewSpec.parent_dimensions:type_name -> rill.runtime.v1.FieldSelector 35, // 55: rill.runtime.v1.MetricsViewSpec.parent_measures:type_name -> rill.runtime.v1.FieldSelector - 85, // 56: rill.runtime.v1.MetricsViewSpec.annotations:type_name -> rill.runtime.v1.MetricsViewSpec.Annotation + 87, // 56: rill.runtime.v1.MetricsViewSpec.annotations:type_name -> rill.runtime.v1.MetricsViewSpec.Annotation 23, // 57: rill.runtime.v1.MetricsViewSpec.security_rules:type_name -> rill.runtime.v1.SecurityRule - 87, // 58: rill.runtime.v1.MetricsViewSpec.query_attributes:type_name -> rill.runtime.v1.MetricsViewSpec.QueryAttributesEntry - 86, // 59: rill.runtime.v1.MetricsViewSpec.rollups:type_name -> rill.runtime.v1.MetricsViewSpec.Rollup + 89, // 58: rill.runtime.v1.MetricsViewSpec.query_attributes:type_name -> rill.runtime.v1.MetricsViewSpec.QueryAttributesEntry + 88, // 59: rill.runtime.v1.MetricsViewSpec.rollups:type_name -> rill.runtime.v1.MetricsViewSpec.Rollup 24, // 60: rill.runtime.v1.SecurityRule.access:type_name -> rill.runtime.v1.SecurityRuleAccess 25, // 61: rill.runtime.v1.SecurityRule.field_access:type_name -> rill.runtime.v1.SecurityRuleFieldAccess 26, // 62: rill.runtime.v1.SecurityRule.row_filter:type_name -> rill.runtime.v1.SecurityRuleRowFilter @@ -9508,10 +9661,10 @@ var file_rill_runtime_v1_resources_proto_depIdxs = []int32{ 10, // 64: rill.runtime.v1.SecurityRuleAccess.condition_resources:type_name -> rill.runtime.v1.ResourceName 10, // 65: rill.runtime.v1.SecurityRuleFieldAccess.condition_resources:type_name -> rill.runtime.v1.ResourceName 10, // 66: rill.runtime.v1.SecurityRuleRowFilter.condition_resources:type_name -> rill.runtime.v1.ResourceName - 96, // 67: rill.runtime.v1.SecurityRuleRowFilter.expression:type_name -> rill.runtime.v1.Expression + 98, // 67: rill.runtime.v1.SecurityRuleRowFilter.expression:type_name -> rill.runtime.v1.Expression 10, // 68: rill.runtime.v1.SecurityRuleTransitiveAccess.resource:type_name -> rill.runtime.v1.ResourceName 22, // 69: rill.runtime.v1.MetricsViewState.valid_spec:type_name -> rill.runtime.v1.MetricsViewSpec - 92, // 70: rill.runtime.v1.MetricsViewState.data_refreshed_on:type_name -> google.protobuf.Timestamp + 94, // 70: rill.runtime.v1.MetricsViewState.data_refreshed_on:type_name -> google.protobuf.Timestamp 30, // 71: rill.runtime.v1.Explore.spec:type_name -> rill.runtime.v1.ExploreSpec 31, // 72: rill.runtime.v1.Explore.state:type_name -> rill.runtime.v1.ExploreState 35, // 73: rill.runtime.v1.ExploreSpec.dimensions_selector:type_name -> rill.runtime.v1.FieldSelector @@ -9521,11 +9674,11 @@ var file_rill_runtime_v1_resources_proto_depIdxs = []int32{ 34, // 77: rill.runtime.v1.ExploreSpec.default_preset:type_name -> rill.runtime.v1.ExplorePreset 23, // 78: rill.runtime.v1.ExploreSpec.security_rules:type_name -> rill.runtime.v1.SecurityRule 30, // 79: rill.runtime.v1.ExploreState.valid_spec:type_name -> rill.runtime.v1.ExploreSpec - 92, // 80: rill.runtime.v1.ExploreState.data_refreshed_on:type_name -> google.protobuf.Timestamp + 94, // 80: rill.runtime.v1.ExploreState.data_refreshed_on:type_name -> google.protobuf.Timestamp 33, // 81: rill.runtime.v1.ExploreTimeRange.comparison_time_ranges:type_name -> rill.runtime.v1.ExploreComparisonTimeRange 35, // 82: rill.runtime.v1.ExplorePreset.dimensions_selector:type_name -> rill.runtime.v1.FieldSelector 35, // 83: rill.runtime.v1.ExplorePreset.measures_selector:type_name -> rill.runtime.v1.FieldSelector - 96, // 84: rill.runtime.v1.ExplorePreset.where:type_name -> rill.runtime.v1.Expression + 98, // 84: rill.runtime.v1.ExplorePreset.where:type_name -> rill.runtime.v1.Expression 2, // 85: rill.runtime.v1.ExplorePreset.comparison_mode:type_name -> rill.runtime.v1.ExploreComparisonMode 3, // 86: rill.runtime.v1.ExplorePreset.view:type_name -> rill.runtime.v1.ExploreWebView 4, // 87: rill.runtime.v1.ExplorePreset.explore_sort_type:type_name -> rill.runtime.v1.ExploreSortType @@ -9534,98 +9687,101 @@ var file_rill_runtime_v1_resources_proto_depIdxs = []int32{ 39, // 90: rill.runtime.v1.Migration.state:type_name -> rill.runtime.v1.MigrationState 41, // 91: rill.runtime.v1.Report.spec:type_name -> rill.runtime.v1.ReportSpec 42, // 92: rill.runtime.v1.Report.state:type_name -> rill.runtime.v1.ReportState - 72, // 93: rill.runtime.v1.ReportSpec.refresh_schedule:type_name -> rill.runtime.v1.Schedule - 93, // 94: rill.runtime.v1.ReportSpec.resolver_properties:type_name -> google.protobuf.Struct - 97, // 95: rill.runtime.v1.ReportSpec.export_format:type_name -> rill.runtime.v1.ExportFormat + 74, // 93: rill.runtime.v1.ReportSpec.refresh_schedule:type_name -> rill.runtime.v1.Schedule + 95, // 94: rill.runtime.v1.ReportSpec.resolver_properties:type_name -> google.protobuf.Struct + 99, // 95: rill.runtime.v1.ReportSpec.export_format:type_name -> rill.runtime.v1.ExportFormat 46, // 96: rill.runtime.v1.ReportSpec.notifiers:type_name -> rill.runtime.v1.Notifier - 88, // 97: rill.runtime.v1.ReportSpec.annotations:type_name -> rill.runtime.v1.ReportSpec.AnnotationsEntry - 92, // 98: rill.runtime.v1.ReportState.next_run_on:type_name -> google.protobuf.Timestamp + 90, // 97: rill.runtime.v1.ReportSpec.annotations:type_name -> rill.runtime.v1.ReportSpec.AnnotationsEntry + 94, // 98: rill.runtime.v1.ReportState.next_run_on:type_name -> google.protobuf.Timestamp 43, // 99: rill.runtime.v1.ReportState.current_execution:type_name -> rill.runtime.v1.ReportExecution 43, // 100: rill.runtime.v1.ReportState.execution_history:type_name -> rill.runtime.v1.ReportExecution - 92, // 101: rill.runtime.v1.ReportExecution.report_time:type_name -> google.protobuf.Timestamp - 92, // 102: rill.runtime.v1.ReportExecution.started_on:type_name -> google.protobuf.Timestamp - 92, // 103: rill.runtime.v1.ReportExecution.finished_on:type_name -> google.protobuf.Timestamp + 94, // 101: rill.runtime.v1.ReportExecution.report_time:type_name -> google.protobuf.Timestamp + 94, // 102: rill.runtime.v1.ReportExecution.started_on:type_name -> google.protobuf.Timestamp + 94, // 103: rill.runtime.v1.ReportExecution.finished_on:type_name -> google.protobuf.Timestamp 45, // 104: rill.runtime.v1.Alert.spec:type_name -> rill.runtime.v1.AlertSpec 47, // 105: rill.runtime.v1.Alert.state:type_name -> rill.runtime.v1.AlertState - 72, // 106: rill.runtime.v1.AlertSpec.refresh_schedule:type_name -> rill.runtime.v1.Schedule - 93, // 107: rill.runtime.v1.AlertSpec.resolver_properties:type_name -> google.protobuf.Struct - 93, // 108: rill.runtime.v1.AlertSpec.query_for_attributes:type_name -> google.protobuf.Struct + 74, // 106: rill.runtime.v1.AlertSpec.refresh_schedule:type_name -> rill.runtime.v1.Schedule + 95, // 107: rill.runtime.v1.AlertSpec.resolver_properties:type_name -> google.protobuf.Struct + 95, // 108: rill.runtime.v1.AlertSpec.query_for_attributes:type_name -> google.protobuf.Struct 46, // 109: rill.runtime.v1.AlertSpec.notifiers:type_name -> rill.runtime.v1.Notifier - 89, // 110: rill.runtime.v1.AlertSpec.annotations:type_name -> rill.runtime.v1.AlertSpec.AnnotationsEntry - 93, // 111: rill.runtime.v1.Notifier.properties:type_name -> google.protobuf.Struct - 92, // 112: rill.runtime.v1.AlertState.next_run_on:type_name -> google.protobuf.Timestamp + 91, // 110: rill.runtime.v1.AlertSpec.annotations:type_name -> rill.runtime.v1.AlertSpec.AnnotationsEntry + 95, // 111: rill.runtime.v1.Notifier.properties:type_name -> google.protobuf.Struct + 94, // 112: rill.runtime.v1.AlertState.next_run_on:type_name -> google.protobuf.Timestamp 48, // 113: rill.runtime.v1.AlertState.current_execution:type_name -> rill.runtime.v1.AlertExecution 48, // 114: rill.runtime.v1.AlertState.execution_history:type_name -> rill.runtime.v1.AlertExecution 49, // 115: rill.runtime.v1.AlertExecution.result:type_name -> rill.runtime.v1.AssertionResult - 92, // 116: rill.runtime.v1.AlertExecution.execution_time:type_name -> google.protobuf.Timestamp - 92, // 117: rill.runtime.v1.AlertExecution.started_on:type_name -> google.protobuf.Timestamp - 92, // 118: rill.runtime.v1.AlertExecution.finished_on:type_name -> google.protobuf.Timestamp - 92, // 119: rill.runtime.v1.AlertExecution.suppressed_since:type_name -> google.protobuf.Timestamp + 94, // 116: rill.runtime.v1.AlertExecution.execution_time:type_name -> google.protobuf.Timestamp + 94, // 117: rill.runtime.v1.AlertExecution.started_on:type_name -> google.protobuf.Timestamp + 94, // 118: rill.runtime.v1.AlertExecution.finished_on:type_name -> google.protobuf.Timestamp + 94, // 119: rill.runtime.v1.AlertExecution.suppressed_since:type_name -> google.protobuf.Timestamp 5, // 120: rill.runtime.v1.AssertionResult.status:type_name -> rill.runtime.v1.AssertionStatus - 93, // 121: rill.runtime.v1.AssertionResult.fail_row:type_name -> google.protobuf.Struct + 95, // 121: rill.runtime.v1.AssertionResult.fail_row:type_name -> google.protobuf.Struct 51, // 122: rill.runtime.v1.RefreshTrigger.spec:type_name -> rill.runtime.v1.RefreshTriggerSpec 52, // 123: rill.runtime.v1.RefreshTrigger.state:type_name -> rill.runtime.v1.RefreshTriggerState 10, // 124: rill.runtime.v1.RefreshTriggerSpec.resources:type_name -> rill.runtime.v1.ResourceName 53, // 125: rill.runtime.v1.RefreshTriggerSpec.models:type_name -> rill.runtime.v1.RefreshModelTrigger 55, // 126: rill.runtime.v1.Theme.spec:type_name -> rill.runtime.v1.ThemeSpec 56, // 127: rill.runtime.v1.Theme.state:type_name -> rill.runtime.v1.ThemeState - 98, // 128: rill.runtime.v1.ThemeSpec.primary_color:type_name -> rill.runtime.v1.Color - 98, // 129: rill.runtime.v1.ThemeSpec.secondary_color:type_name -> rill.runtime.v1.Color + 100, // 128: rill.runtime.v1.ThemeSpec.primary_color:type_name -> rill.runtime.v1.Color + 100, // 129: rill.runtime.v1.ThemeSpec.secondary_color:type_name -> rill.runtime.v1.Color 57, // 130: rill.runtime.v1.ThemeSpec.light:type_name -> rill.runtime.v1.ThemeColors 57, // 131: rill.runtime.v1.ThemeSpec.dark:type_name -> rill.runtime.v1.ThemeColors - 90, // 132: rill.runtime.v1.ThemeColors.variables:type_name -> rill.runtime.v1.ThemeColors.VariablesEntry + 92, // 132: rill.runtime.v1.ThemeColors.variables:type_name -> rill.runtime.v1.ThemeColors.VariablesEntry 59, // 133: rill.runtime.v1.Component.spec:type_name -> rill.runtime.v1.ComponentSpec 60, // 134: rill.runtime.v1.Component.state:type_name -> rill.runtime.v1.ComponentState - 93, // 135: rill.runtime.v1.ComponentSpec.renderer_properties:type_name -> google.protobuf.Struct + 95, // 135: rill.runtime.v1.ComponentSpec.renderer_properties:type_name -> google.protobuf.Struct 61, // 136: rill.runtime.v1.ComponentSpec.input:type_name -> rill.runtime.v1.ComponentVariable 61, // 137: rill.runtime.v1.ComponentSpec.output:type_name -> rill.runtime.v1.ComponentVariable 59, // 138: rill.runtime.v1.ComponentState.valid_spec:type_name -> rill.runtime.v1.ComponentSpec - 92, // 139: rill.runtime.v1.ComponentState.data_refreshed_on:type_name -> google.protobuf.Timestamp - 99, // 140: rill.runtime.v1.ComponentVariable.default_value:type_name -> google.protobuf.Value + 94, // 139: rill.runtime.v1.ComponentState.data_refreshed_on:type_name -> google.protobuf.Timestamp + 101, // 140: rill.runtime.v1.ComponentVariable.default_value:type_name -> google.protobuf.Value 63, // 141: rill.runtime.v1.Canvas.spec:type_name -> rill.runtime.v1.CanvasSpec 64, // 142: rill.runtime.v1.Canvas.state:type_name -> rill.runtime.v1.CanvasState 55, // 143: rill.runtime.v1.CanvasSpec.embedded_theme:type_name -> rill.runtime.v1.ThemeSpec 32, // 144: rill.runtime.v1.CanvasSpec.time_ranges:type_name -> rill.runtime.v1.ExploreTimeRange - 67, // 145: rill.runtime.v1.CanvasSpec.default_preset:type_name -> rill.runtime.v1.CanvasPreset + 69, // 145: rill.runtime.v1.CanvasSpec.default_preset:type_name -> rill.runtime.v1.CanvasPreset 61, // 146: rill.runtime.v1.CanvasSpec.variables:type_name -> rill.runtime.v1.ComponentVariable 65, // 147: rill.runtime.v1.CanvasSpec.rows:type_name -> rill.runtime.v1.CanvasRow 23, // 148: rill.runtime.v1.CanvasSpec.security_rules:type_name -> rill.runtime.v1.SecurityRule 63, // 149: rill.runtime.v1.CanvasState.valid_spec:type_name -> rill.runtime.v1.CanvasSpec - 92, // 150: rill.runtime.v1.CanvasState.data_refreshed_on:type_name -> google.protobuf.Timestamp - 66, // 151: rill.runtime.v1.CanvasRow.items:type_name -> rill.runtime.v1.CanvasItem - 2, // 152: rill.runtime.v1.CanvasPreset.comparison_mode:type_name -> rill.runtime.v1.ExploreComparisonMode - 91, // 153: rill.runtime.v1.CanvasPreset.filter_expr:type_name -> rill.runtime.v1.CanvasPreset.FilterExprEntry - 96, // 154: rill.runtime.v1.DefaultMetricsSQLFilter.expression:type_name -> rill.runtime.v1.Expression - 70, // 155: rill.runtime.v1.API.spec:type_name -> rill.runtime.v1.APISpec - 71, // 156: rill.runtime.v1.API.state:type_name -> rill.runtime.v1.APIState - 93, // 157: rill.runtime.v1.APISpec.resolver_properties:type_name -> google.protobuf.Struct - 23, // 158: rill.runtime.v1.APISpec.security_rules:type_name -> rill.runtime.v1.SecurityRule - 77, // 159: rill.runtime.v1.ParseError.start_location:type_name -> rill.runtime.v1.CharLocation - 79, // 160: rill.runtime.v1.ConnectorV2.spec:type_name -> rill.runtime.v1.ConnectorSpec - 80, // 161: rill.runtime.v1.ConnectorV2.state:type_name -> rill.runtime.v1.ConnectorState - 93, // 162: rill.runtime.v1.ConnectorSpec.properties:type_name -> google.protobuf.Struct - 93, // 163: rill.runtime.v1.ConnectorSpec.provision_args:type_name -> google.protobuf.Struct - 6, // 164: rill.runtime.v1.MetricsViewSpec.Dimension.type:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionType - 95, // 165: rill.runtime.v1.MetricsViewSpec.Dimension.smallest_time_grain:type_name -> rill.runtime.v1.TimeGrain - 100, // 166: rill.runtime.v1.MetricsViewSpec.Dimension.data_type:type_name -> rill.runtime.v1.Type - 95, // 167: rill.runtime.v1.MetricsViewSpec.DimensionSelector.time_grain:type_name -> rill.runtime.v1.TimeGrain - 82, // 168: rill.runtime.v1.MetricsViewSpec.MeasureWindow.order_by:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector - 7, // 169: rill.runtime.v1.MetricsViewSpec.Measure.type:type_name -> rill.runtime.v1.MetricsViewSpec.MeasureType - 83, // 170: rill.runtime.v1.MetricsViewSpec.Measure.window:type_name -> rill.runtime.v1.MetricsViewSpec.MeasureWindow - 82, // 171: rill.runtime.v1.MetricsViewSpec.Measure.per_dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector - 82, // 172: rill.runtime.v1.MetricsViewSpec.Measure.required_dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector - 93, // 173: rill.runtime.v1.MetricsViewSpec.Measure.format_d3_locale:type_name -> google.protobuf.Struct - 100, // 174: rill.runtime.v1.MetricsViewSpec.Measure.data_type:type_name -> rill.runtime.v1.Type - 35, // 175: rill.runtime.v1.MetricsViewSpec.Annotation.measures_selector:type_name -> rill.runtime.v1.FieldSelector - 95, // 176: rill.runtime.v1.MetricsViewSpec.Rollup.time_grain:type_name -> rill.runtime.v1.TimeGrain - 35, // 177: rill.runtime.v1.MetricsViewSpec.Rollup.dimensions_selector:type_name -> rill.runtime.v1.FieldSelector - 35, // 178: rill.runtime.v1.MetricsViewSpec.Rollup.measures_selector:type_name -> rill.runtime.v1.FieldSelector - 68, // 179: rill.runtime.v1.CanvasPreset.FilterExprEntry.value:type_name -> rill.runtime.v1.DefaultMetricsSQLFilter - 180, // [180:180] is the sub-list for method output_type - 180, // [180:180] is the sub-list for method input_type - 180, // [180:180] is the sub-list for extension type_name - 180, // [180:180] is the sub-list for extension extendee - 0, // [0:180] is the sub-list for field type_name + 94, // 150: rill.runtime.v1.CanvasState.data_refreshed_on:type_name -> google.protobuf.Timestamp + 68, // 151: rill.runtime.v1.CanvasRow.items:type_name -> rill.runtime.v1.CanvasItem + 66, // 152: rill.runtime.v1.CanvasRow.tab_group:type_name -> rill.runtime.v1.CanvasTabGroup + 67, // 153: rill.runtime.v1.CanvasTabGroup.tabs:type_name -> rill.runtime.v1.CanvasTab + 65, // 154: rill.runtime.v1.CanvasTab.rows:type_name -> rill.runtime.v1.CanvasRow + 2, // 155: rill.runtime.v1.CanvasPreset.comparison_mode:type_name -> rill.runtime.v1.ExploreComparisonMode + 93, // 156: rill.runtime.v1.CanvasPreset.filter_expr:type_name -> rill.runtime.v1.CanvasPreset.FilterExprEntry + 98, // 157: rill.runtime.v1.DefaultMetricsSQLFilter.expression:type_name -> rill.runtime.v1.Expression + 72, // 158: rill.runtime.v1.API.spec:type_name -> rill.runtime.v1.APISpec + 73, // 159: rill.runtime.v1.API.state:type_name -> rill.runtime.v1.APIState + 95, // 160: rill.runtime.v1.APISpec.resolver_properties:type_name -> google.protobuf.Struct + 23, // 161: rill.runtime.v1.APISpec.security_rules:type_name -> rill.runtime.v1.SecurityRule + 79, // 162: rill.runtime.v1.ParseError.start_location:type_name -> rill.runtime.v1.CharLocation + 81, // 163: rill.runtime.v1.ConnectorV2.spec:type_name -> rill.runtime.v1.ConnectorSpec + 82, // 164: rill.runtime.v1.ConnectorV2.state:type_name -> rill.runtime.v1.ConnectorState + 95, // 165: rill.runtime.v1.ConnectorSpec.properties:type_name -> google.protobuf.Struct + 95, // 166: rill.runtime.v1.ConnectorSpec.provision_args:type_name -> google.protobuf.Struct + 6, // 167: rill.runtime.v1.MetricsViewSpec.Dimension.type:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionType + 97, // 168: rill.runtime.v1.MetricsViewSpec.Dimension.smallest_time_grain:type_name -> rill.runtime.v1.TimeGrain + 102, // 169: rill.runtime.v1.MetricsViewSpec.Dimension.data_type:type_name -> rill.runtime.v1.Type + 97, // 170: rill.runtime.v1.MetricsViewSpec.DimensionSelector.time_grain:type_name -> rill.runtime.v1.TimeGrain + 84, // 171: rill.runtime.v1.MetricsViewSpec.MeasureWindow.order_by:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector + 7, // 172: rill.runtime.v1.MetricsViewSpec.Measure.type:type_name -> rill.runtime.v1.MetricsViewSpec.MeasureType + 85, // 173: rill.runtime.v1.MetricsViewSpec.Measure.window:type_name -> rill.runtime.v1.MetricsViewSpec.MeasureWindow + 84, // 174: rill.runtime.v1.MetricsViewSpec.Measure.per_dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector + 84, // 175: rill.runtime.v1.MetricsViewSpec.Measure.required_dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector + 95, // 176: rill.runtime.v1.MetricsViewSpec.Measure.format_d3_locale:type_name -> google.protobuf.Struct + 102, // 177: rill.runtime.v1.MetricsViewSpec.Measure.data_type:type_name -> rill.runtime.v1.Type + 35, // 178: rill.runtime.v1.MetricsViewSpec.Annotation.measures_selector:type_name -> rill.runtime.v1.FieldSelector + 97, // 179: rill.runtime.v1.MetricsViewSpec.Rollup.time_grain:type_name -> rill.runtime.v1.TimeGrain + 35, // 180: rill.runtime.v1.MetricsViewSpec.Rollup.dimensions_selector:type_name -> rill.runtime.v1.FieldSelector + 35, // 181: rill.runtime.v1.MetricsViewSpec.Rollup.measures_selector:type_name -> rill.runtime.v1.FieldSelector + 70, // 182: rill.runtime.v1.CanvasPreset.FilterExprEntry.value:type_name -> rill.runtime.v1.DefaultMetricsSQLFilter + 183, // [183:183] is the sub-list for method output_type + 183, // [183:183] is the sub-list for method input_type + 183, // [183:183] is the sub-list for extension type_name + 183, // [183:183] is the sub-list for extension extendee + 0, // [0:183] is the sub-list for field type_name } func init() { file_rill_runtime_v1_resources_proto_init() } @@ -10336,7 +10492,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[58].Exporter = func(v any, i int) any { - switch v := v.(*CanvasItem); i { + switch v := v.(*CanvasTabGroup); i { case 0: return &v.state case 1: @@ -10348,7 +10504,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[59].Exporter = func(v any, i int) any { - switch v := v.(*CanvasPreset); i { + switch v := v.(*CanvasTab); i { case 0: return &v.state case 1: @@ -10360,7 +10516,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[60].Exporter = func(v any, i int) any { - switch v := v.(*DefaultMetricsSQLFilter); i { + switch v := v.(*CanvasItem); i { case 0: return &v.state case 1: @@ -10372,7 +10528,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[61].Exporter = func(v any, i int) any { - switch v := v.(*API); i { + switch v := v.(*CanvasPreset); i { case 0: return &v.state case 1: @@ -10384,7 +10540,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[62].Exporter = func(v any, i int) any { - switch v := v.(*APISpec); i { + switch v := v.(*DefaultMetricsSQLFilter); i { case 0: return &v.state case 1: @@ -10396,7 +10552,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[63].Exporter = func(v any, i int) any { - switch v := v.(*APIState); i { + switch v := v.(*API); i { case 0: return &v.state case 1: @@ -10408,7 +10564,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[64].Exporter = func(v any, i int) any { - switch v := v.(*Schedule); i { + switch v := v.(*APISpec); i { case 0: return &v.state case 1: @@ -10420,7 +10576,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[65].Exporter = func(v any, i int) any { - switch v := v.(*ParseError); i { + switch v := v.(*APIState); i { case 0: return &v.state case 1: @@ -10432,7 +10588,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[66].Exporter = func(v any, i int) any { - switch v := v.(*ValidationError); i { + switch v := v.(*Schedule); i { case 0: return &v.state case 1: @@ -10444,7 +10600,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[67].Exporter = func(v any, i int) any { - switch v := v.(*DependencyError); i { + switch v := v.(*ParseError); i { case 0: return &v.state case 1: @@ -10456,7 +10612,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[68].Exporter = func(v any, i int) any { - switch v := v.(*ExecutionError); i { + switch v := v.(*ValidationError); i { case 0: return &v.state case 1: @@ -10468,7 +10624,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[69].Exporter = func(v any, i int) any { - switch v := v.(*CharLocation); i { + switch v := v.(*DependencyError); i { case 0: return &v.state case 1: @@ -10480,7 +10636,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[70].Exporter = func(v any, i int) any { - switch v := v.(*ConnectorV2); i { + switch v := v.(*ExecutionError); i { case 0: return &v.state case 1: @@ -10492,7 +10648,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[71].Exporter = func(v any, i int) any { - switch v := v.(*ConnectorSpec); i { + switch v := v.(*CharLocation); i { case 0: return &v.state case 1: @@ -10504,7 +10660,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[72].Exporter = func(v any, i int) any { - switch v := v.(*ConnectorState); i { + switch v := v.(*ConnectorV2); i { case 0: return &v.state case 1: @@ -10516,7 +10672,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[73].Exporter = func(v any, i int) any { - switch v := v.(*MetricsViewSpec_Dimension); i { + switch v := v.(*ConnectorSpec); i { case 0: return &v.state case 1: @@ -10528,7 +10684,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[74].Exporter = func(v any, i int) any { - switch v := v.(*MetricsViewSpec_DimensionSelector); i { + switch v := v.(*ConnectorState); i { case 0: return &v.state case 1: @@ -10540,7 +10696,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[75].Exporter = func(v any, i int) any { - switch v := v.(*MetricsViewSpec_MeasureWindow); i { + switch v := v.(*MetricsViewSpec_Dimension); i { case 0: return &v.state case 1: @@ -10552,7 +10708,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[76].Exporter = func(v any, i int) any { - switch v := v.(*MetricsViewSpec_Measure); i { + switch v := v.(*MetricsViewSpec_DimensionSelector); i { case 0: return &v.state case 1: @@ -10564,7 +10720,7 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[77].Exporter = func(v any, i int) any { - switch v := v.(*MetricsViewSpec_Annotation); i { + switch v := v.(*MetricsViewSpec_MeasureWindow); i { case 0: return &v.state case 1: @@ -10576,6 +10732,30 @@ func file_rill_runtime_v1_resources_proto_init() { } } file_rill_runtime_v1_resources_proto_msgTypes[78].Exporter = func(v any, i int) any { + switch v := v.(*MetricsViewSpec_Measure); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rill_runtime_v1_resources_proto_msgTypes[79].Exporter = func(v any, i int) any { + switch v := v.(*MetricsViewSpec_Annotation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rill_runtime_v1_resources_proto_msgTypes[80].Exporter = func(v any, i int) any { switch v := v.(*MetricsViewSpec_Rollup); i { case 0: return &v.state @@ -10627,15 +10807,15 @@ func file_rill_runtime_v1_resources_proto_init() { } file_rill_runtime_v1_resources_proto_msgTypes[47].OneofWrappers = []any{} file_rill_runtime_v1_resources_proto_msgTypes[57].OneofWrappers = []any{} - file_rill_runtime_v1_resources_proto_msgTypes[58].OneofWrappers = []any{} - file_rill_runtime_v1_resources_proto_msgTypes[59].OneofWrappers = []any{} + file_rill_runtime_v1_resources_proto_msgTypes[60].OneofWrappers = []any{} + file_rill_runtime_v1_resources_proto_msgTypes[61].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_rill_runtime_v1_resources_proto_rawDesc, NumEnums: 8, - NumMessages: 84, + NumMessages: 86, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/gen/rill/runtime/v1/resources.pb.validate.go b/proto/gen/rill/runtime/v1/resources.pb.validate.go index f25ac1785efb..3586df94009e 100644 --- a/proto/gen/rill/runtime/v1/resources.pb.validate.go +++ b/proto/gen/rill/runtime/v1/resources.pb.validate.go @@ -10714,6 +10714,35 @@ func (m *CanvasRow) validate(all bool) error { } + if all { + switch v := interface{}(m.GetTabGroup()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CanvasRowValidationError{ + field: "TabGroup", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CanvasRowValidationError{ + field: "TabGroup", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetTabGroup()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CanvasRowValidationError{ + field: "TabGroup", + reason: "embedded message failed validation", + cause: err, + } + } + } + if m.Height != nil { // no validation rules for Height } @@ -10795,6 +10824,279 @@ var _ interface { ErrorName() string } = CanvasRowValidationError{} +// Validate checks the field values on CanvasTabGroup with the rules defined in +// the proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *CanvasTabGroup) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CanvasTabGroup with the rules defined +// in the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in CanvasTabGroupMultiError, +// or nil if none found. +func (m *CanvasTabGroup) ValidateAll() error { + return m.validate(true) +} + +func (m *CanvasTabGroup) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Name + + for idx, item := range m.GetTabs() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CanvasTabGroupValidationError{ + field: fmt.Sprintf("Tabs[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CanvasTabGroupValidationError{ + field: fmt.Sprintf("Tabs[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CanvasTabGroupValidationError{ + field: fmt.Sprintf("Tabs[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return CanvasTabGroupMultiError(errors) + } + + return nil +} + +// CanvasTabGroupMultiError is an error wrapping multiple validation errors +// returned by CanvasTabGroup.ValidateAll() if the designated constraints +// aren't met. +type CanvasTabGroupMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CanvasTabGroupMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CanvasTabGroupMultiError) AllErrors() []error { return m } + +// CanvasTabGroupValidationError is the validation error returned by +// CanvasTabGroup.Validate if the designated constraints aren't met. +type CanvasTabGroupValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CanvasTabGroupValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CanvasTabGroupValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CanvasTabGroupValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CanvasTabGroupValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CanvasTabGroupValidationError) ErrorName() string { return "CanvasTabGroupValidationError" } + +// Error satisfies the builtin error interface +func (e CanvasTabGroupValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCanvasTabGroup.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CanvasTabGroupValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CanvasTabGroupValidationError{} + +// Validate checks the field values on CanvasTab with the rules defined in the +// proto definition for this message. If any rules are violated, the first +// error encountered is returned, or nil if there are no violations. +func (m *CanvasTab) Validate() error { + return m.validate(false) +} + +// ValidateAll checks the field values on CanvasTab with the rules defined in +// the proto definition for this message. If any rules are violated, the +// result is a list of violation errors wrapped in CanvasTabMultiError, or nil +// if none found. +func (m *CanvasTab) ValidateAll() error { + return m.validate(true) +} + +func (m *CanvasTab) validate(all bool) error { + if m == nil { + return nil + } + + var errors []error + + // no validation rules for Name + + // no validation rules for DisplayName + + for idx, item := range m.GetRows() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, CanvasTabValidationError{ + field: fmt.Sprintf("Rows[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, CanvasTabValidationError{ + field: fmt.Sprintf("Rows[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return CanvasTabValidationError{ + field: fmt.Sprintf("Rows[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + + if len(errors) > 0 { + return CanvasTabMultiError(errors) + } + + return nil +} + +// CanvasTabMultiError is an error wrapping multiple validation errors returned +// by CanvasTab.ValidateAll() if the designated constraints aren't met. +type CanvasTabMultiError []error + +// Error returns a concatenation of all the error messages it wraps. +func (m CanvasTabMultiError) Error() string { + var msgs []string + for _, err := range m { + msgs = append(msgs, err.Error()) + } + return strings.Join(msgs, "; ") +} + +// AllErrors returns a list of validation violation errors. +func (m CanvasTabMultiError) AllErrors() []error { return m } + +// CanvasTabValidationError is the validation error returned by +// CanvasTab.Validate if the designated constraints aren't met. +type CanvasTabValidationError struct { + field string + reason string + cause error + key bool +} + +// Field function returns field value. +func (e CanvasTabValidationError) Field() string { return e.field } + +// Reason function returns reason value. +func (e CanvasTabValidationError) Reason() string { return e.reason } + +// Cause function returns cause value. +func (e CanvasTabValidationError) Cause() error { return e.cause } + +// Key function returns key value. +func (e CanvasTabValidationError) Key() bool { return e.key } + +// ErrorName returns error name. +func (e CanvasTabValidationError) ErrorName() string { return "CanvasTabValidationError" } + +// Error satisfies the builtin error interface +func (e CanvasTabValidationError) Error() string { + cause := "" + if e.cause != nil { + cause = fmt.Sprintf(" | caused by: %v", e.cause) + } + + key := "" + if e.key { + key = "key for " + } + + return fmt.Sprintf( + "invalid %sCanvasTab.%s: %s%s", + key, + e.field, + e.reason, + cause) +} + +var _ error = CanvasTabValidationError{} + +var _ interface { + Field() string + Reason() string + Key() bool + Cause() error + ErrorName() string +} = CanvasTabValidationError{} + // Validate checks the field values on CanvasItem with the rules defined in the // proto definition for this message. If any rules are violated, the first // error encountered is returned, or nil if there are no violations. diff --git a/proto/gen/rill/runtime/v1/runtime.swagger.yaml b/proto/gen/rill/runtime/v1/runtime.swagger.yaml index ec9e2a8b9086..6f4b046fff55 100644 --- a/proto/gen/rill/runtime/v1/runtime.swagger.yaml +++ b/proto/gen/rill/runtime/v1/runtime.swagger.yaml @@ -4370,7 +4370,12 @@ definitions: items: type: object $ref: '#/definitions/v1CanvasItem' - description: Items to render in the row. + description: Items to render in the row. Empty when the row is a tab group. + tabGroup: + $ref: '#/definitions/v1CanvasTabGroup' + description: |- + If set, this row renders a tab group instead of items. + A row has either items or a tab_group, never both. v1CanvasSpec: type: object properties: @@ -4466,6 +4471,37 @@ definitions: description: |- The last time any underlying metrics view(s)'s data was refreshed. This may be empty if the data refresh time is not known, e.g. if the metrics view is based on an externally managed table. + v1CanvasTab: + type: object + properties: + name: + type: string + description: Stable identifier for the tab, used for URL state. Derived from the label. + displayName: + type: string + description: User-facing label for the tab. + rows: + type: array + items: + type: object + $ref: '#/definitions/v1CanvasRow' + description: |- + Rows to render when the tab is active. These are always plain rows; + a tab's rows never contain a nested tab_group. + v1CanvasTabGroup: + type: object + properties: + name: + type: string + description: |- + Stable identifier for the tab group, used for URL state. + Defaults to "group-" if not provided in the canvas YAML. + tabs: + type: array + items: + type: object + $ref: '#/definitions/v1CanvasTab' + description: Tabs in the group. A group always has at least one tab. v1CategoricalSummary: type: object properties: diff --git a/proto/rill/runtime/v1/resources.proto b/proto/rill/runtime/v1/resources.proto index 8e9e162be4ff..2f85f9ff12a6 100644 --- a/proto/rill/runtime/v1/resources.proto +++ b/proto/rill/runtime/v1/resources.proto @@ -900,8 +900,29 @@ message CanvasRow { optional uint32 height = 1; // Unit of the height. Current possible values: "px", empty string. string height_unit = 2; - // Items to render in the row. + // Items to render in the row. Empty when the row is a tab group. repeated CanvasItem items = 3; + // If set, this row renders a tab group instead of items. + // A row has either items or a tab_group, never both. + CanvasTabGroup tab_group = 4; +} + +message CanvasTabGroup { + // Stable identifier for the tab group, used for URL state. + // Defaults to "group-" if not provided in the canvas YAML. + string name = 1; + // Tabs in the group. A group always has at least one tab. + repeated CanvasTab tabs = 2; +} + +message CanvasTab { + // Stable identifier for the tab, used for URL state. Derived from the label. + string name = 1; + // User-facing label for the tab. + string display_name = 2; + // Rows to render when the tab is active. These are always plain rows; + // a tab's rows never contain a nested tab_group. + repeated CanvasRow rows = 3; } message CanvasItem { diff --git a/runtime/canvases.go b/runtime/canvases.go index 901e2b1a2bdf..e55ee01f4e41 100644 --- a/runtime/canvases.go +++ b/runtime/canvases.go @@ -10,6 +10,21 @@ import ( "github.com/rilldata/rill/runtime/metricsview/metricssql" ) +// CollectCanvasComponentNames collects the names of all components referenced by the given rows, +// descending into tab groups (one level deep, since tabs cannot be nested). +func CollectCanvasComponentNames(rows []*runtimev1.CanvasRow, out map[string]bool) { + for _, row := range rows { + for _, item := range row.Items { + out[item.Component] = true + } + if tg := row.GetTabGroup(); tg != nil { + for _, tab := range tg.Tabs { + CollectCanvasComponentNames(tab.Rows, out) + } + } + } +} + type ResolveCanvasResult struct { Canvas *runtimev1.Resource ResolvedComponents map[string]*runtimev1.Resource @@ -51,25 +66,22 @@ func (r *Runtime) ResolveCanvas(ctx context.Context, instanceID, canvas string, components := make(map[string]*runtimev1.Resource) - for _, row := range spec.Rows { - for _, item := range row.Items { - // Skip if already resolved. - if _, ok := components[item.Component]; ok { - continue - } + // Collect all referenced component names, descending into tab groups. + componentNames := make(map[string]bool) + CollectCanvasComponentNames(spec.Rows, componentNames) - // Get component resource. - cmp, err := ctrl.Get(ctx, &runtimev1.ResourceName{Kind: ResourceKindComponent, Name: item.Component}, false) - if err != nil { - if errors.Is(err, drivers.ErrResourceNotFound) { - return nil, fmt.Errorf("component %q in valid spec not found", item.Component) - } - return nil, err + for componentName := range componentNames { + // Get component resource. + cmp, err := ctrl.Get(ctx, &runtimev1.ResourceName{Kind: ResourceKindComponent, Name: componentName}, false) + if err != nil { + if errors.Is(err, drivers.ErrResourceNotFound) { + return nil, fmt.Errorf("component %q in valid spec not found", componentName) } - - // Add to map without resolving templates. Use ResolveTemplatedString RPC for template resolution. - components[item.Component] = cmp + return nil, err } + + // Add to map without resolving templates. Use ResolveTemplatedString RPC for template resolution. + components[componentName] = cmp } // Extract metrics view names from components diff --git a/runtime/parser/parse_canvas.go b/runtime/parser/parse_canvas.go index ddb7863b9f18..6dc5dc9d45a6 100644 --- a/runtime/parser/parse_canvas.go +++ b/runtime/parser/parse_canvas.go @@ -41,15 +41,32 @@ type CanvasYAML struct { Filters map[string]string `yaml:"filters"` } `yaml:"defaults"` Variables []*ComponentVariableYAML `yaml:"variables"` - Rows []*struct { - Height *string `yaml:"height"` - Items []*struct { - Width *string `yaml:"width"` - Component string `yaml:"component"` // Name of an externally defined component - InlineComponent map[string]yaml.Node `yaml:",inline"` // Any other properties are considered an inline component definition - } `yaml:"items"` - } - Security *SecurityPolicyYAML `yaml:"security"` + Rows []*canvasRowYAML `yaml:"rows"` + Security *SecurityPolicyYAML `yaml:"security"` +} + +// canvasRowYAML is a single entry in a canvas's (or tab's) rows list. +// It is either a plain row (items) or a tab group (tabs), never both. +type canvasRowYAML struct { + Height *string `yaml:"height"` + Items []*canvasItemYAML `yaml:"items"` + // Name is the stable identifier of a tab group. Only used for tab-group entries. + Name string `yaml:"name"` + // Tabs, when set, makes this entry a tab group instead of a plain row. + Tabs []*canvasTabYAML `yaml:"tabs"` +} + +// canvasTabYAML is a single tab within a tab group. +type canvasTabYAML struct { + Label string `yaml:"label"` + Rows []*canvasRowYAML `yaml:"rows"` +} + +// canvasItemYAML is a single item within a row. +type canvasItemYAML struct { + Width *string `yaml:"width"` + Component string `yaml:"component"` // Name of an externally defined component + InlineComponent map[string]yaml.Node `yaml:",inline"` // Any other properties are considered an inline component definition } func (p *Parser) parseCanvas(node *Node) error { @@ -134,84 +151,11 @@ func (p *Parser) parseCanvas(node *Node) error { } // Parse rows and items. - // Items have position and size, and either reference an externally defined component by name or define a component inline. - var rows []*runtimev1.CanvasRow + // Each row entry is either a plain row (items) or a tab group (tabs); tab groups are only allowed at the top level. var inlineComponentDefs []*componentDef // Track inline component definitions so we can insert them after we have validated all components - for i, row := range tmp.Rows { - if row == nil { - return fmt.Errorf("row at index %d is empty", i) - } - - var height *uint32 - var heightUnit string - if row.Height != nil { - v, u, err := parseItemSize(*row.Height) - if err != nil { - return fmt.Errorf("invalid height for row %d: %w", i, err) - } - if v != 0 && u != "px" { - return fmt.Errorf("invalid height unit %q for row %d: unit must be 'px'", u, i) - } - height = &v - heightUnit = u - } - - var items []*runtimev1.CanvasItem - for j, item := range row.Items { - if item == nil { - return fmt.Errorf("item %d in row %d is empty", j, i) - } - - var width *uint32 - var widthUnit string - if item.Width != nil { - v, u, err := parseItemSize(*item.Width) - if err != nil { - return fmt.Errorf("invalid width for item %d in row %d: %w", j, i, err) - } - if u != "" { - return fmt.Errorf("invalid width unit %q for item %d in row %d: 'width' cannot have a unit", u, j, i) - } - width = &v - widthUnit = u - } - - // Validate that exactly one of Component and InlineComponent are set - if item.Component == "" && len(item.InlineComponent) == 0 { - return fmt.Errorf("item %d in row %d is missing a component definition", j, i) - } - if item.Component != "" && len(item.InlineComponent) > 0 { - return fmt.Errorf("item %d in row %d has properties incompatible with 'component'", j, i) - } - - // Parse inline component definition if present and assign into item.Component - var definedInCanvs bool - if len(item.InlineComponent) > 0 { - name, def, err := p.parseCanvasInlineComponent(node.Name, i, j, item.InlineComponent) - if err != nil { - return fmt.Errorf("invalid component for item %d in row %d: %w", j, i, err) - } - - item.Component = name - inlineComponentDefs = append(inlineComponentDefs, def) - definedInCanvs = true - } - - items = append(items, &runtimev1.CanvasItem{ - Component: item.Component, - DefinedInCanvas: definedInCanvs, - Width: width, - WidthUnit: widthUnit, - }) - - node.Refs = append(node.Refs, ResourceName{Kind: ResourceKindComponent, Name: item.Component}) - } - - rows = append(rows, &runtimev1.CanvasRow{ - Height: height, - HeightUnit: heightUnit, - Items: items, - }) + rows, err := p.parseCanvasRows(node, tmp.Rows, true, "", &inlineComponentDefs) + if err != nil { + return err } // Build and validate presets @@ -306,12 +250,200 @@ func (p *Parser) parseCanvas(node *Node) error { return nil } +// parseCanvasRows parses a list of canvas row entries. Each entry is either a plain row (items) +// or a tab group (tabs). Tab groups are only allowed when allowTabs is true (the top level); +// a tab's own rows are always plain. posPrefix disambiguates inline component names across tabs. +func (p *Parser) parseCanvasRows(node *Node, rows []*canvasRowYAML, allowTabs bool, posPrefix string, inlineComponentDefs *[]*componentDef) ([]*runtimev1.CanvasRow, error) { + var out []*runtimev1.CanvasRow + // seenGroupNames tracks tab group names so each group has a unique URL key. Only populated at the top level. + seenGroupNames := make(map[string]bool) + for i, row := range rows { + if row == nil { + return nil, fmt.Errorf("row at index %d is empty", i) + } + + // Dispatch on whether this entry is a tab group. Presence of the `tabs:` key (even if empty) + // marks an entry as a group, so an empty `tabs: []` is rejected rather than silently treated as a row. + if row.Tabs != nil { + if len(row.Items) > 0 { + return nil, fmt.Errorf("row %d cannot define both 'items' and 'tabs'", i) + } + if !allowTabs { + return nil, fmt.Errorf("tab groups cannot be nested inside a tab (row %d)", i) + } + group, err := p.parseCanvasTabGroup(node, row, i, posPrefix, seenGroupNames, inlineComponentDefs) + if err != nil { + return nil, err + } + out = append(out, &runtimev1.CanvasRow{TabGroup: group}) + continue + } + + var height *uint32 + var heightUnit string + if row.Height != nil { + v, u, err := parseItemSize(*row.Height) + if err != nil { + return nil, fmt.Errorf("invalid height for row %d: %w", i, err) + } + if v != 0 && u != "px" { + return nil, fmt.Errorf("invalid height unit %q for row %d: unit must be 'px'", u, i) + } + height = &v + heightUnit = u + } + + var items []*runtimev1.CanvasItem + for j, item := range row.Items { + if item == nil { + return nil, fmt.Errorf("item %d in row %d is empty", j, i) + } + + var width *uint32 + var widthUnit string + if item.Width != nil { + v, u, err := parseItemSize(*item.Width) + if err != nil { + return nil, fmt.Errorf("invalid width for item %d in row %d: %w", j, i, err) + } + if u != "" { + return nil, fmt.Errorf("invalid width unit %q for item %d in row %d: 'width' cannot have a unit", u, j, i) + } + width = &v + widthUnit = u + } + + // Validate that exactly one of Component and InlineComponent are set + if item.Component == "" && len(item.InlineComponent) == 0 { + return nil, fmt.Errorf("item %d in row %d is missing a component definition", j, i) + } + if item.Component != "" && len(item.InlineComponent) > 0 { + return nil, fmt.Errorf("item %d in row %d has properties incompatible with 'component'", j, i) + } + + // Parse inline component definition if present and assign into item.Component + var definedInCanvs bool + if len(item.InlineComponent) > 0 { + name, def, err := p.parseCanvasInlineComponent(node.Name, fmt.Sprintf("%s%d-%d", posPrefix, i, j), item.InlineComponent) + if err != nil { + return nil, fmt.Errorf("invalid component for item %d in row %d: %w", j, i, err) + } + + item.Component = name + *inlineComponentDefs = append(*inlineComponentDefs, def) + definedInCanvs = true + } + + items = append(items, &runtimev1.CanvasItem{ + Component: item.Component, + DefinedInCanvas: definedInCanvs, + Width: width, + WidthUnit: widthUnit, + }) + + node.Refs = append(node.Refs, ResourceName{Kind: ResourceKindComponent, Name: item.Component}) + } + + out = append(out, &runtimev1.CanvasRow{ + Height: height, + HeightUnit: heightUnit, + Items: items, + }) + } + + return out, nil +} + +// parseCanvasTabGroup parses a single tab group entry (a row with tabs). +func (p *Parser) parseCanvasTabGroup(node *Node, row *canvasRowYAML, rowIdx int, posPrefix string, seenGroupNames map[string]bool, inlineComponentDefs *[]*componentDef) (*runtimev1.CanvasTabGroup, error) { + if len(row.Tabs) == 0 { + return nil, fmt.Errorf("tab group at row %d must have at least one tab", rowIdx) + } + + groupName := row.Name + if groupName == "" { + groupName = fmt.Sprintf("group-%d", rowIdx) + } + if seenGroupNames[groupName] { + return nil, fmt.Errorf("duplicate tab group name %q at row %d", groupName, rowIdx) + } + seenGroupNames[groupName] = true + + var tabs []*runtimev1.CanvasTab + seenNames := make(map[string]bool, len(row.Tabs)) + for t, tab := range row.Tabs { + if tab == nil { + return nil, fmt.Errorf("tab %d in tab group at row %d is empty", t, rowIdx) + } + if tab.Label == "" { + return nil, fmt.Errorf("tab %d in tab group at row %d is missing a label", t, rowIdx) + } + + // Derive a stable, URL-safe name from the label, uniquified against earlier tabs in this group. + tabName := uniqueName(slugify(tab.Label), fmt.Sprintf("tab-%d", t), seenNames) + seenNames[tabName] = true + + tabRows, err := p.parseCanvasRows(node, tab.Rows, false, fmt.Sprintf("%sg%d-t%d-", posPrefix, rowIdx, t), inlineComponentDefs) + if err != nil { + return nil, fmt.Errorf("invalid tab %q in tab group at row %d: %w", tab.Label, rowIdx, err) + } + + tabs = append(tabs, &runtimev1.CanvasTab{ + Name: tabName, + DisplayName: tab.Label, + Rows: tabRows, + }) + } + + return &runtimev1.CanvasTabGroup{ + Name: groupName, + Tabs: tabs, + }, nil +} + +// uniqueName returns name if it is non-empty and unused, otherwise it derives a unique +// alternative: first the supplied fallback, then fallback with a numeric suffix. +func uniqueName(name, fallback string, seen map[string]bool) string { + if name == "" { + name = fallback + } + if !seen[name] { + return name + } + for n := 2; ; n++ { + candidate := fmt.Sprintf("%s-%d", name, n) + if !seen[candidate] { + return candidate + } + } +} + +// slugify converts a label into a lowercase, URL-safe identifier. +func slugify(s string) string { + var b strings.Builder + prevDash := false + for _, r := range strings.ToLower(strings.TrimSpace(s)) { + switch { + case r >= 'a' && r <= 'z', r >= '0' && r <= '9': + b.WriteRune(r) + prevDash = false + default: + if !prevDash && b.Len() > 0 { + b.WriteRune('-') + prevDash = true + } + } + } + return strings.Trim(b.String(), "-") +} + // parseCanvasInlineComponent parses an inline component definition in a canvas item. -func (p *Parser) parseCanvasInlineComponent(canvasName string, rowIdx, itemIdx int, props map[string]yaml.Node) (string, *componentDef, error) { +// posKey uniquely identifies the item's position in the canvas (including any tab path). +func (p *Parser) parseCanvasInlineComponent(canvasName, posKey string, props map[string]yaml.Node) (string, *componentDef, error) { var n yaml.Node err := n.Encode(props) if err != nil { - return "", nil, fmt.Errorf("invalid component for item %d in row %d: %w", itemIdx, rowIdx, err) + return "", nil, fmt.Errorf("invalid component at %s: %w", posKey, err) } tmp := &ComponentYAML{} @@ -327,11 +459,11 @@ func (p *Parser) parseCanvasInlineComponent(canvasName string, rowIdx, itemIdx i spec.DefinedInCanvas = true - name := fmt.Sprintf("%s--component-%d-%d", canvasName, rowIdx, itemIdx) + name := fmt.Sprintf("%s--component-%s", canvasName, posKey) err = p.insertDryRun(ResourceKindComponent, name) if err != nil { - name = fmt.Sprintf("%s--component-%d-%d-%s", canvasName, rowIdx, itemIdx, uuid.New()) + name = fmt.Sprintf("%s--component-%s-%s", canvasName, posKey, uuid.New()) err = p.insertDryRun(ResourceKindComponent, name) if err != nil { return "", nil, err diff --git a/runtime/parser/parser_test.go b/runtime/parser/parser_test.go index a99f550a1f96..65c9e1c6ab3e 100644 --- a/runtime/parser/parser_test.go +++ b/runtime/parser/parser_test.go @@ -1885,6 +1885,223 @@ rows: requireResourcesAndErrors(t, p, resources, nil) } +func TestCanvasTabGroups(t *testing.T) { + ctx := context.Background() + repo := makeRepo(t, map[string]string{ + `rill.yaml`: ``, + `components/c1.yaml`: ` +type: component +kpi: + metrics_view: foo +`, + `canvases/d1.yaml`: ` +type: canvas +rows: +- items: + - component: c1 +- name: deep_dive + tabs: + - label: Overview + rows: + - items: + - markdown: + content: "Overview" + - label: Detail View + rows: + - items: + - markdown: + content: "Detail" +`, + }) + + resources := []*Resource{ + { + Name: ResourceName{Kind: ResourceKindComponent, Name: "c1"}, + Paths: []string{"/components/c1.yaml"}, + Refs: []ResourceName{{Kind: ResourceKindMetricsView, Name: "foo"}}, + ComponentSpec: &runtimev1.ComponentSpec{ + DisplayName: "C1", + Renderer: "kpi", + RendererProperties: must(structpb.NewStruct(map[string]any{"metrics_view": "foo"})), + }, + }, + { + Name: ResourceName{Kind: ResourceKindComponent, Name: "d1--component-g1-t0-0-0"}, + Paths: []string{"/canvases/d1.yaml"}, + ComponentSpec: &runtimev1.ComponentSpec{ + Renderer: "markdown", + RendererProperties: must(structpb.NewStruct(map[string]any{"content": "Overview"})), + DefinedInCanvas: true, + }, + }, + { + Name: ResourceName{Kind: ResourceKindComponent, Name: "d1--component-g1-t1-0-0"}, + Paths: []string{"/canvases/d1.yaml"}, + ComponentSpec: &runtimev1.ComponentSpec{ + Renderer: "markdown", + RendererProperties: must(structpb.NewStruct(map[string]any{"content": "Detail"})), + DefinedInCanvas: true, + }, + }, + { + Name: ResourceName{Kind: ResourceKindCanvas, Name: "d1"}, + Paths: []string{"/canvases/d1.yaml"}, + Refs: []ResourceName{ + {Kind: ResourceKindComponent, Name: "c1"}, + {Kind: ResourceKindComponent, Name: "d1--component-g1-t0-0-0"}, + {Kind: ResourceKindComponent, Name: "d1--component-g1-t1-0-0"}, + }, + CanvasSpec: &runtimev1.CanvasSpec{ + DisplayName: "D1", + AllowCustomTimeRange: true, + FiltersEnabled: true, + Rows: []*runtimev1.CanvasRow{ + { + Items: []*runtimev1.CanvasItem{ + {Component: "c1"}, + }, + }, + { + TabGroup: &runtimev1.CanvasTabGroup{ + Name: "deep_dive", + Tabs: []*runtimev1.CanvasTab{ + { + Name: "overview", + DisplayName: "Overview", + Rows: []*runtimev1.CanvasRow{ + {Items: []*runtimev1.CanvasItem{{Component: "d1--component-g1-t0-0-0", DefinedInCanvas: true}}}, + }, + }, + { + Name: "detail-view", + DisplayName: "Detail View", + Rows: []*runtimev1.CanvasRow{ + {Items: []*runtimev1.CanvasItem{{Component: "d1--component-g1-t1-0-0", DefinedInCanvas: true}}}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + p, err := Parse(ctx, repo, "", "", "duckdb", true) + require.NoError(t, err) + requireResourcesAndErrors(t, p, resources, nil) +} + +func TestCanvasTabGroupErrors(t *testing.T) { + ctx := context.Background() + + cases := []struct { + name string + yaml string + }{ + { + name: "items and tabs together", + yaml: ` +type: canvas +rows: +- items: + - markdown: + content: "x" + tabs: + - label: A + rows: [] +`, + }, + { + name: "nested tab groups", + yaml: ` +type: canvas +rows: +- tabs: + - label: Outer + rows: + - tabs: + - label: Inner + rows: [] +`, + }, + { + name: "empty tab group", + yaml: ` +type: canvas +rows: +- name: empty + tabs: [] +`, + }, + { + name: "duplicate group names", + yaml: ` +type: canvas +rows: +- name: dup + tabs: + - label: A + rows: [] +- name: dup + tabs: + - label: B + rows: [] +`, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + repo := makeRepo(t, map[string]string{ + `rill.yaml`: ``, + `canvases/d1.yaml`: tc.yaml, + }) + p, err := Parse(ctx, repo, "", "", "duckdb", true) + require.NoError(t, err) + require.Len(t, p.Resources, 0) + require.Len(t, p.Errors, 1) + }) + } +} + +// TestCanvasTabNameUniqueness verifies that tab names are uniquified when labels slugify +// to the same value, so each tab keeps a distinct URL key. +func TestCanvasTabNameUniqueness(t *testing.T) { + ctx := context.Background() + repo := makeRepo(t, map[string]string{ + `rill.yaml`: ``, + `canvases/d1.yaml`: ` +type: canvas +rows: +- tabs: + - label: Sales + rows: [] + - label: "Sales!" + rows: [] + - label: "Sales?" + rows: [] +`, + }) + + p, err := Parse(ctx, repo, "", "", "duckdb", true) + require.NoError(t, err) + require.Len(t, p.Errors, 0) + + var canvas *runtimev1.CanvasSpec + for _, r := range p.Resources { + if r.CanvasSpec != nil { + canvas = r.CanvasSpec + } + } + require.NotNil(t, canvas) + + tabs := canvas.Rows[0].TabGroup.Tabs + require.Equal(t, "sales", tabs[0].Name) + require.Equal(t, "sales-2", tabs[1].Name) + require.Equal(t, "sales-3", tabs[2].Name) +} + func TestKindBackwardsCompatibility(t *testing.T) { files := map[string]string{ // rill.yaml diff --git a/runtime/parser/schema/project.schema.yaml b/runtime/parser/schema/project.schema.yaml index 913ab9c26533..b692472f7c07 100644 --- a/runtime/parser/schema/project.schema.yaml +++ b/runtime/parser/schema/project.schema.yaml @@ -2373,7 +2373,7 @@ definitions: description: Refers to the custom banner displayed at the header of an Canvas dashboard rows: type: array - description: Refers to all of the rows displayed on the Canvas + description: Refers to all of the rows displayed on the Canvas. Each entry is either a plain row (with `items`) or a tab group (with `tabs`), but not both. items: type: object properties: @@ -2391,7 +2391,7 @@ definitions: description: | Name of the component to display. Each component type has its own set of properties. Available component types: - + - **markdown** - Text component, uses markdown formatting - **kpi_grid** - KPI component, similar to TDD in Rill Explore, display quick KPI charts - **stacked_bar_normalized** - Bar chart normalized to 100% values @@ -2409,6 +2409,22 @@ definitions: - integer description: Width of the component (can be a number or string with unit) additionalProperties: true + name: + type: string + description: Stable identifier for a tab group, used as its deep-link URL key. Defaults to `group-` if omitted. Only used for tab-group entries. + tabs: + type: array + description: Makes this entry a tab group instead of a plain row. Only the active tab's rows render; tabs cannot be nested. + items: + type: object + properties: + label: + type: string + description: User-facing tab label. A URL-safe name is derived from it for deep-linking. + rows: + type: array + description: Plain rows (with `items`) shown when this tab is active. Tab rows cannot themselves contain `tabs`. + additionalProperties: false additionalProperties: false max_width: type: integer diff --git a/runtime/reconcilers/canvas.go b/runtime/reconcilers/canvas.go index 3f3a696c2bc9..6962504b3705 100644 --- a/runtime/reconcilers/canvas.go +++ b/runtime/reconcilers/canvas.go @@ -187,13 +187,9 @@ func (r *CanvasReconciler) ResolveTransitiveAccess(ctx context.Context, claims * return nil, fmt.Errorf("failed to get controller: %w", err) } - // Collect all component names referenced by the canvas + // Collect all component names referenced by the canvas (including those nested in tab groups) componentNames := make(map[string]bool) - for _, row := range spec.Rows { - for _, item := range row.Items { - componentNames[item.Component] = true - } - } + runtime.CollectCanvasComponentNames(spec.Rows, componentNames) // Process each component for componentName := range componentNames { diff --git a/web-common/src/features/canvas/AddComponentDropdown.svelte b/web-common/src/features/canvas/AddComponentDropdown.svelte index c40b9332cb74..9ae1836ba110 100644 --- a/web-common/src/features/canvas/AddComponentDropdown.svelte +++ b/web-common/src/features/canvas/AddComponentDropdown.svelte @@ -5,7 +5,7 @@ VISIBLE_CHART_TYPES, } from "@rilldata/web-common/features/components/charts/config"; import { featureFlags } from "@rilldata/web-common/features/feature-flags"; - import { Plus, PlusCircle } from "lucide-svelte"; + import { Layers, Plus, PlusCircle } from "lucide-svelte"; import type { ComponentType, SvelteComponent } from "svelte"; import type { ChartType } from "../components/charts/types"; import type { CanvasComponentType } from "./components/types"; @@ -33,12 +33,17 @@ export let disabled = false; export let componentForm = false; export let floatingForm = false; + // Label shown on the large (componentForm) add button. + export let label = "Add widget"; export let open = false; export let rowIndex: number | undefined = undefined; export let columnIndex: number | undefined = undefined; export let onItemClick: (type: CanvasComponentType) => void; export let onMouseEnter: () => void = () => {}; export let onOpenChange: (isOpen: boolean) => void = () => {}; + // When provided, the menu offers "Tab group" as a final item. Only passed at the + // top level (tab groups cannot be nested inside a tab or a column). + export let onAddTabGroup: (() => void) | undefined = undefined; const { customCharts } = featureFlags; @@ -63,7 +68,7 @@ class="pointer-events-auto shadow-sm hover:shadow-md flex bg-surface-subtle h-[84px] flex-col justify-center gap-2 items-center rounded-md border border-gray-200 w-full" > - Add widget + {label} {:else if floatingForm} + {/snippet} + + + + {#snippet child({ props })} + + {/snippet} + + + + +{/if} + diff --git a/web-common/src/features/canvas/CanvasTabStrip.svelte b/web-common/src/features/canvas/CanvasTabStrip.svelte new file mode 100644 index 000000000000..172caeab4803 --- /dev/null +++ b/web-common/src/features/canvas/CanvasTabStrip.svelte @@ -0,0 +1,165 @@ + + +
+
+ {#each $tabs as tab, index (tab.name)} + + {/each} + + {#if editable} + + {/if} +
+
+ + diff --git a/web-common/src/features/canvas/EditableCanvasRow.svelte b/web-common/src/features/canvas/EditableCanvasRow.svelte index 9a13483ec856..07421b89c87e 100644 --- a/web-common/src/features/canvas/EditableCanvasRow.svelte +++ b/web-common/src/features/canvas/EditableCanvasRow.svelte @@ -17,6 +17,7 @@ } from "./layout-util"; import RowDropZone from "./RowDropZone.svelte"; import RowWrapper from "./RowWrapper.svelte"; + import { rowColFromPath } from "./stores/canvas-entity"; import type { Row } from "./stores/row"; import { activeDivider } from "./stores/ui-stores"; @@ -40,6 +41,10 @@ }) => void; export let onDuplicate: (params: { columnIndex: number }) => void; export let onDelete: (params: { component: BaseCanvasComponent }) => void; + // Optional: convert this row into a tab group (top-level rows only). + export let onConvertToTabGroup: (() => void) | undefined = undefined; + // Optional: insert a tab group at a given top-level index (top-level rows only). + export let onAddTabGroup: ((index: number) => void) | undefined = undefined; export let onDrop: (row: number, column: number | null) => void; export let initializeRow: (row: number, type: CanvasComponentType) => void; export let updateRowHeight: (newHeight: number, index: number) => void; @@ -47,6 +52,9 @@ index: number, newWidths: number[], ) => void; + // Disambiguates row DOM ids across tab containers so height-resize querySelectors + // don't collide with same-index rows elsewhere on the canvas. + export let idPrefix: string = ""; let rowHeight = get(row.height) ?? MIN_HEIGHT; let hasLocalChange = false; @@ -66,7 +74,7 @@ $: updateHeightFromSpec($height); $: updateWidthsFromSpec($itemWidths); - $: id = `canvas-row-${rowIndex}`; + $: id = `canvas-row-${idPrefix}${rowIndex}`; function onRowResizeStart() { initialMousePosition = $mousePosition; @@ -146,8 +154,7 @@ const width = widths[columnIndex]; initialHeight = - document.querySelector(`#canvas-row-${rowIndex}`)?.getBoundingClientRect() - .height ?? + document.querySelector(`#${id}`)?.getBoundingClientRect().height ?? rowHeight ?? MIN_HEIGHT; @@ -219,7 +226,10 @@ row={rowIndex} maxColumns={itemCount} allowDrop={activelyDragging && - (itemCount < 4 || dragComponent?.pathInYAML?.[1] === rowIndex)} + (itemCount < 4 || + (dragComponent + ? rowColFromPath(dragComponent.pathInYAML).row === rowIndex + : false))} {onDrop} /> @@ -242,6 +252,9 @@ onDelete={() => { onDelete({ component }); }} + onConvertToTabGroup={onConvertToTabGroup + ? () => onConvertToTabGroup?.() + : undefined} /> {:else} @@ -257,6 +270,9 @@ addItem={(type) => { initializeRow(rowIndex + 1, type); }} + onAddTabGroup={onAddTabGroup + ? () => onAddTabGroup?.(rowIndex + 1) + : undefined} /> {#if rowIndex === 0} @@ -267,6 +283,7 @@ addItem={(type) => { initializeRow(rowIndex, type); }} + onAddTabGroup={onAddTabGroup ? () => onAddTabGroup?.(0) : undefined} /> {/if} diff --git a/web-common/src/features/canvas/EditableCanvasTabGroup.svelte b/web-common/src/features/canvas/EditableCanvasTabGroup.svelte new file mode 100644 index 000000000000..b46e4fd2facd --- /dev/null +++ b/web-common/src/features/canvas/EditableCanvasTabGroup.svelte @@ -0,0 +1,196 @@ + + +
+ onAddTab(blockIndex)} + onRenameTab={(tabIndex, label) => onRenameTab(blockIndex, tabIndex, label)} + onDeleteTab={(tabIndex) => onDeleteTab(blockIndex, tabIndex)} + onMoveTab={(tabIndex, direction) => + onMoveTab(blockIndex, tabIndex, direction)} + onReorderTab={(from, to) => onReorderTab(blockIndex, from, to)} + onDropOnTab={(tabIndex) => onDropOnTab(blockIndex, tabIndex)} + /> + + {#if activeTab && grid} + {#each $grid as row, rowIndex (rowIndex)} + onDrop(r, c, target)} + addItems={(pos, items) => addItems(pos, items, target)} + spreadEvenly={(index) => spreadEvenly(index, target)} + initializeRow={(r, type) => initializeRow(r, type, target)} + updateRowHeight={(h, index) => updateRowHeight(h, index, target)} + updateComponentWidths={(index, widths) => + updateComponentWidths(index, widths, target)} + {onComponentMouseDown} + onDuplicate={({ columnIndex }) => + onDuplicate(rowIndex, columnIndex, target)} + {onDelete} + /> + {/each} + + + + + {#if hasValidMetrics} + initializeRow($grid.length, type, target)} + /> + {:else} + + {/if} + + + {/if} +
+ +{#if isLastBlock && hasValidMetrics} + + + + initializeRow(blockIndex + 1, type)} + onAddTabGroup={() => onAddTabGroup(blockIndex + 1)} + /> + + +{/if} + + diff --git a/web-common/src/features/canvas/RowDropZone.svelte b/web-common/src/features/canvas/RowDropZone.svelte index 01748761ac1f..4f1c45cda777 100644 --- a/web-common/src/features/canvas/RowDropZone.svelte +++ b/web-common/src/features/canvas/RowDropZone.svelte @@ -10,6 +10,8 @@ export let onDrop: (row: number, column: number | null) => void; export let onRowResizeStart: (e: MouseEvent) => void = () => {}; export let addItem: (type: CanvasComponentType) => void; + // When provided, the add menu offers inserting a tab group at this position. + export let onAddTabGroup: (() => void) | undefined = undefined; let menuOpen = false; @@ -88,6 +90,7 @@ } }} onItemClick={addItem} + {onAddTabGroup} /> diff --git a/web-common/src/features/canvas/StaticCanvasRow.svelte b/web-common/src/features/canvas/StaticCanvasRow.svelte index be327a411617..c2d30424debd 100644 --- a/web-common/src/features/canvas/StaticCanvasRow.svelte +++ b/web-common/src/features/canvas/StaticCanvasRow.svelte @@ -15,6 +15,7 @@ export let heightUnit: string = "px"; export let navigationEnabled: boolean = true; export let activeComponentId: string | null = null; + export let idPrefix: string = ""; $: ({ height, items: _itemIds, widths: itemWidths } = row); @@ -22,7 +23,7 @@ $: itemIds = $_itemIds; - $: id = `canvas-row-${rowIndex}`; + $: id = `canvas-row-${idPrefix}${rowIndex}`; void; export let onDuplicate: () => void; + // Optional: convert this component's row into a tab group. Only provided for + // top-level rows (a tab's rows cannot be nested into another tab group). + export let onConvertToTabGroup: (() => void) | undefined = undefined; export let editable = false; export let component: BaseCanvasComponent; export let navigationEnabled: boolean = true; @@ -63,6 +66,12 @@ Duplicate + {#if onConvertToTabGroup} + + + Convert row to tab group + + {/if} {#if showExplore && exploreComponent} diff --git a/web-common/src/features/canvas/components/charts/BaseChart.ts b/web-common/src/features/canvas/components/charts/BaseChart.ts index 7fe986a7f388..5512f5cd093f 100644 --- a/web-common/src/features/canvas/components/charts/BaseChart.ts +++ b/web-common/src/features/canvas/components/charts/BaseChart.ts @@ -34,7 +34,12 @@ import type { CommonChartProperties, FieldConfig, } from "../../../components/charts/types"; -import type { CanvasEntity, ComponentPath } from "../../stores/canvas-entity"; +import { + rowColFromPath, + type CanvasEntity, + type ComponentPath, +} from "../../stores/canvas-entity"; +import { namePrefixFromPath } from "../../layout-util"; import type { ComponentCommonProperties, ComponentFilterProperties, @@ -187,13 +192,15 @@ export abstract class BaseChart< ...commonProps, }; + const { row, col } = rowColFromPath(this.pathInYAML); const newResource = this.parent.createOptimisticResource({ type: key, - row: this.pathInYAML[1], - column: this.pathInYAML[3], + row, + column: col, metricsViewName: currentSpec.metrics_view, metricsViewSpec, spec: mergedSpec, + namePrefix: namePrefixFromPath(this.pathInYAML), }); const newComponent = createComponent( diff --git a/web-common/src/features/canvas/components/charts/custom-chart/chart-ai-agent.ts b/web-common/src/features/canvas/components/charts/custom-chart/chart-ai-agent.ts index 6c76e03ab1b0..4c6e9f7e4c85 100644 --- a/web-common/src/features/canvas/components/charts/custom-chart/chart-ai-agent.ts +++ b/web-common/src/features/canvas/components/charts/custom-chart/chart-ai-agent.ts @@ -6,6 +6,23 @@ import type { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { derived, get, type Readable } from "svelte/store"; import type { CustomChartComponent } from "./index"; +/** + * Format a component's YAML path (minus the trailing renderer-type segment) into a string + * the dev agent can locate unambiguously, e.g. "rows[1].tabs[0].rows[2].items[0]" for a + * tabbed component or "rows[3].items[1]" for a top-level one. + */ +function formatYamlPath(path: (string | number)[]): string { + return path + .map((segment, i) => + typeof segment === "number" + ? `[${segment}]` + : i === 0 + ? segment + : `.${segment}`, + ) + .join(""); +} + const componentConversations = new Map(); export function clearComponentConversation(componentId: string): void { @@ -21,9 +38,11 @@ function buildPrompt( ): string { const canvasName = component.parent.name; const canvasFilePath = `/dashboards/${canvasName}.yaml`; - const [, rowIdx, , colIdx] = component.pathInYAML; + // Drop the trailing renderer-type segment; the remainder is the item's YAML path. Reading + // from the end (rather than fixed indices) keeps this correct for components nested in tabs. + const yamlPath = formatYamlPath(component.pathInYAML.slice(0, -1)); - return `In the canvas dashboard file ${canvasFilePath}, update the custom_chart component at row ${rowIdx}, item ${colIdx}. Write the metrics_sql and vega_spec for this chart: ${userPrompt}`; + return `In the canvas dashboard file ${canvasFilePath}, update the custom_chart component at YAML path ${yamlPath}. Write the metrics_sql and vega_spec for this chart: ${userPrompt}`; } /** diff --git a/web-common/src/features/canvas/layout-util.spec.ts b/web-common/src/features/canvas/layout-util.spec.ts new file mode 100644 index 000000000000..b8b0bda3e413 --- /dev/null +++ b/web-common/src/features/canvas/layout-util.spec.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from "vitest"; +import { parseDocument } from "yaml"; +import { generateNewAssets, mapGuard, rowsGuard } from "./layout-util"; + +// A canvas with one free row followed by a tab group that contains a widget. +const CANVAS = `type: canvas +rows: + - items: + - component: header + - name: deep_dive + tabs: + - label: Overview + rows: + - items: + - component: d1--component-g1-t0-0-0 +`; + +describe("mapGuard", () => { + it("preserves tab group rows instead of coercing them to empty items", () => { + const doc = parseDocument(CANVAS); + const rows = mapGuard(rowsGuard(doc.getIn(["rows"]))); + + expect(rows).toHaveLength(2); + expect(rows[0].items).toEqual([{ component: "header" }]); + // The tab group row keeps its tabs and has no coerced `items`. + expect(rows[1].items).toBeUndefined(); + expect(rows[1].name).toBe("deep_dive"); + expect(rows[1].tabs).toBeDefined(); + }); +}); + +describe("generateNewAssets with a tab group present", () => { + it("preserves the tab group when a top-level row is added before it", () => { + const doc = parseDocument(CANVAS); + const yamlRows = mapGuard(rowsGuard(doc.getIn(["rows"]))); + const specRows = [ + { items: [{ component: "header" }] }, + { + tabGroup: { + name: "deep_dive", + tabs: [ + { + name: "overview", + displayName: "Overview", + rows: [{ items: [{ component: "d1--component-g1-t0-0-0" }] }], + }, + ], + }, + }, + ]; + + const { newYamlRows, newSpecRows } = generateNewAssets({ + transaction: { + operations: [ + { + type: "add", + insertRow: true, + componentType: "markdown", + destination: { row: 1, col: 0 }, + }, + ], + }, + yamlRows, + specRows, + resolvedComponents: {}, + canvasName: "d1", + defaultMetrics: { metricsViewName: "foo", metricsViewSpec: undefined }, + }); + + // The new row was inserted and the tab group survived (it was previously stripped + // to empty items and deleted by the cleanup step). + const tabRow = newYamlRows.find((r) => r.tabs !== undefined); + expect(tabRow).toBeDefined(); + expect(newYamlRows).toHaveLength(3); + + const specTabRow = newSpecRows.find((r) => r.tabGroup); + expect( + specTabRow?.tabGroup?.tabs?.[0]?.rows?.[0]?.items?.[0]?.component, + ).toBe("d1--component-g1-t0-0-0"); + }); +}); diff --git a/web-common/src/features/canvas/layout-util.ts b/web-common/src/features/canvas/layout-util.ts index 06746e1ed937..832158a65a68 100644 --- a/web-common/src/features/canvas/layout-util.ts +++ b/web-common/src/features/canvas/layout-util.ts @@ -56,8 +56,12 @@ type YAMLItem = Record & { }; export type YAMLRow = { - items: YAMLItem[]; + items?: YAMLItem[]; height?: string; + // A top-level entry may instead be a tab group (carries `tabs` and an optional `name`). + // These are passed through row transactions untouched so their content is preserved. + tabs?: unknown; + name?: string; }; export type DragItem = { @@ -76,7 +80,14 @@ export function rowsGuard(value: unknown): unknown[] { export function mapGuard(value: unknown[]): Array { return value.map((el) => { if (el instanceof YAMLMap) { - const jsonObject = el.toJSON() as Partial; + const jsonObject = el.toJSON() as YAMLRow; + + // Preserve tab group rows verbatim. Coercing them to `items: []` would strip their + // tabs and the empty-items cleanup would then delete the row, destroying the group + // whenever an unrelated top-level row transaction runs. + if (jsonObject?.tabs !== undefined) { + return jsonObject; + } return { items: jsonObject?.items ?? [], @@ -96,6 +107,36 @@ interface Position { col: number; } +// Identifies an editable rows container. undefined targets the top-level rows; a tab target +// scopes editing to one tab's rows (at YAML path rows[blockIndex].tabs[tabIndex].rows). +export type EditTarget = { blockIndex: number; tabIndex: number }; + +/** YAML path to a tab's rows sequence. */ +export function tabRowsPath(blockIndex: number, tabIndex: number) { + return ["rows", blockIndex, "tabs", tabIndex, "rows"]; +} + +/** Component name prefix for a tab, matching the parser's position key (see parse_canvas.go). */ +export function tabNamePrefix(blockIndex: number, tabIndex: number) { + return `g${blockIndex}-t${tabIndex}-`; +} + +/** Derive the tab target a component path lives in, or undefined for a top-level component. */ +export function tabTargetFromPath( + path: (string | number)[], +): EditTarget | undefined { + if (path.length >= 5 && path[0] === "rows" && path[2] === "tabs") { + return { blockIndex: Number(path[1]), tabIndex: Number(path[3]) }; + } + return undefined; +} + +/** Component name prefix for the tab a path lives in, or "" for a top-level component. */ +export function namePrefixFromPath(path: (string | number)[]): string { + const target = tabTargetFromPath(path); + return target ? tabNamePrefix(target.blockIndex, target.tabIndex) : ""; +} + interface BaseTransaction { insertRow?: boolean; } @@ -272,12 +313,16 @@ export function generateArrayRearrangeFunction(transaction: Transaction) { }; } -function generateId( +// Generates the resource name for a component at a position. namePrefix disambiguates +// components nested in tabs (e.g. "g2-t0-") so they don't collide with top-level ones; +// it mirrors the position key used by the parser (see parse_canvas.go). +export function generateId( row: number | undefined, column: number | undefined, canvasName: string, + namePrefix = "", ) { - return `${canvasName}--component-${row ?? 0}-${column ?? 0}`; + return `${canvasName}--component-${namePrefix}${row ?? 0}-${column ?? 0}`; } export function generateNewAssets(params: { @@ -286,6 +331,8 @@ export function generateNewAssets(params: { specRows: V1CanvasRow[]; resolvedComponents: V1ResolveCanvasResponseResolvedComponents | undefined; canvasName: string; + // Prefix for generated component names, to keep tab components unique. Default "" (top level). + namePrefix?: string; defaultMetrics: { metricsViewName: string; metricsViewSpec: V1MetricsViewSpec | undefined; @@ -296,6 +343,7 @@ export function generateNewAssets(params: { specRows, defaultMetrics, canvasName, + namePrefix = "", resolvedComponents, transaction, } = params; @@ -311,10 +359,14 @@ export function generateNewAssets(params: { }); const resolvedComponentsArray = specRows.map((row) => { - const items = - row.items?.map((item) => { - return resolvedComponents?.[item?.component ?? ""]; - }) ?? []; + // Preserve tab group rows (no items) so this array stays index-aligned with the spec + // and YAML arrays through the cleanup step; their tab components remain resolvable via + // the existing resolvedComponents map that updateAssets merges in. + if (!row.items) + return { ...row, items: undefined as V1Resource[] | undefined }; + const items = row.items.map((item) => { + return resolvedComponents?.[item?.component ?? ""]; + }); return { ...row, items: items.filter(itemExists) }; }); @@ -333,11 +385,12 @@ export function generateNewAssets(params: { }; }, (row, _, touched) => { - if (!touched) return row; - const updatedItems = row.items.map((item) => { + if (!touched || !row.items) return row; + const items = row.items; + const updatedItems = items.map((item) => { return { ...item, - width: touched ? COLUMN_COUNT / row.items.length : item.width, + width: touched ? COLUMN_COUNT / items.length : item.width, }; }); @@ -360,7 +413,7 @@ export function generateNewAssets(params: { }, (row, index, touched) => { const updatedItems = row.items?.map((item, col) => { - item.component = generateId(index, col, canvasName); + item.component = generateId(index, col, canvasName, namePrefix); return { ...item, @@ -375,7 +428,7 @@ export function generateNewAssets(params: { }, ); - const updatedResolvedComponents = mover( + const updatedResolvedComponents = mover( resolvedComponentsArray, (pos, type, operationIndex) => { const spec = getAddedComponentSpec( @@ -391,9 +444,9 @@ export function generateNewAssets(params: { }); }, (row, index) => { - const updatedItems = row.items.map((item, col) => { + const updatedItems = row.items?.map((item, col) => { if (!item?.meta?.name) return item; - item.meta.name.name = generateId(index, col, canvasName); + item.meta.name.name = generateId(index, col, canvasName, namePrefix); return item; }); return { @@ -406,7 +459,7 @@ export function generateNewAssets(params: { const resolvedComponentsMap: Record = {}; updatedResolvedComponents.forEach((row) => { - row.items.forEach((item) => { + row.items?.forEach((item) => { if (item?.meta?.name?.name) { resolvedComponentsMap[item?.meta?.name?.name] = item; } diff --git a/web-common/src/features/canvas/stores/canvas-entity.ts b/web-common/src/features/canvas/stores/canvas-entity.ts index cbf52bd20818..38aba70d1f6b 100644 --- a/web-common/src/features/canvas/stores/canvas-entity.ts +++ b/web-common/src/features/canvas/stores/canvas-entity.ts @@ -9,6 +9,7 @@ import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryCl import { V1ExploreComparisonMode, type V1CanvasPreset, + type V1CanvasRow, type V1CanvasSpec, type V1ComponentSpecRendererProperties, type V1MetricsView, @@ -39,18 +40,22 @@ import { import { FilterManager, flattenExpression } from "./filter-manager"; import { getFilterParam } from "./filter-state"; import { Grid } from "./grid"; +import { TabGroup, type LayoutBlock } from "./tab-group"; import { getComparisonTypeFromRangeString } from "./time-state"; import { TimeManager } from "./time-manager"; import { Theme } from "../../themes/theme"; import { createResolvedThemeStore } from "../../themes/selectors"; import { ExploreStateURLParams } from "../../dashboards/url-state/url-params"; -import { DEFAULT_DASHBOARD_WIDTH } from "../layout-util"; +import { DEFAULT_DASHBOARD_WIDTH, namePrefixFromPath } from "../layout-util"; import { createCustomMapStore } from "@rilldata/web-common/lib/custom-map-store"; import type { RuntimeClient } from "@rilldata/web-common/runtime-client/v2"; import { queryServiceConvertExpressionToMetricsSQL } from "@rilldata/web-common/runtime-client"; export const lastVisitedState = new Map(); +// URL param encoding each tab group's active tab as comma-separated "group:tab" pairs. +export const CANVAS_TABS_URL_PARAM = "tabs"; + // Store for managing URL search parameters // Which may be in the URL or in the Canvas YAML // Set returns a boolean indicating whether the value was set @@ -68,6 +73,11 @@ export type SearchParamsStore = { export class CanvasEntity { componentsStore = createCustomMapStore(); _rows: Grid = new Grid(this); + // Ordered list of top-level layout blocks (plain rows and tab groups). + // For untabbed canvases this mirrors _rows one-to-one; tab groups are rendered from here. + layout = writable([]); + // Tab groups keyed by their stable name, reused across spec updates so active-tab state survives. + private tabGroups = new Map(); // Time state controls timeManager: TimeManager; @@ -607,8 +617,10 @@ export class CanvasEntity { const component = this.componentsStore.getNonReactive(id); if (!component) return; const { pathInYAML, type, resource } = component; - const [, rowIndex, , columnIndex] = pathInYAML; - const path = constructPath(rowIndex, columnIndex, type); + const { row: rowIndex, col: columnIndex } = rowColFromPath(pathInYAML); + // Preserve any tab/group prefix so the duplicate lands in the same container. + const prefix = pathInYAML.slice(0, -4); + const path = constructPath(rowIndex, columnIndex, type, prefix); const existingResource = get(resource); @@ -633,6 +645,7 @@ export class CanvasEntity { column: columnIndex, metricsViewName, metricsViewSpec, + namePrefix: namePrefixFromPath(pathInYAML), }); const newComponent = createComponent(newResource, this, path); @@ -648,46 +661,67 @@ export class CanvasEntity { if (!rows) return; const set = new Set(); - let createdNewComponent = false; const isFirstLoad = get(this.firstLoad); - rows.forEach((row, rowIndex) => { - const items = row.items ?? []; - - items.forEach((item, columnIndex) => { - const componentName = item.component; + // Create/update component instances for a list of rows, descending into tab groups. + // The prefix is the YAML path at which the rows live (top level is ["rows"]). + const processRowItems = ( + rowList: V1CanvasRow[], + prefix: (string | number)[], + ) => { + rowList.forEach((row, rowIndex) => { + if (row.tabGroup) { + // The spec uses the proto shape (row.tabGroup.tabs), but the YAML path + // omits the tab_group wrapper (row.tabs[t].rows) — see parser canvasRowYAML. + row.tabGroup.tabs?.forEach((tab, tabIndex) => { + processRowItems(tab.rows ?? [], [ + ...prefix, + rowIndex, + "tabs", + tabIndex, + "rows", + ]); + }); + return; + } - if (!componentName) return; + const items = row.items ?? []; + items.forEach((item, columnIndex) => { + const componentName = item.component; + if (!componentName) return; - set.add(componentName ?? ""); + set.add(componentName); - const newResource = newComponents?.[componentName]; - if (!newResource) { - throw new Error("No component found: " + componentName); - } + const newResource = newComponents?.[componentName]; + if (!newResource) { + throw new Error("No component found: " + componentName); + } - const newType = (newResource.component?.state?.validSpec?.renderer ?? - (this.allowUnvalidatedSpec - ? newResource.component?.spec?.renderer - : undefined)) as CanvasComponentType; - const existingClass = - this.componentsStore.getNonReactive(componentName); - const path = constructPath(rowIndex, columnIndex, newType); - - if (existingClass && areSameType(newType, existingClass.type)) { - existingClass.update(newResource, path); - } else { - createdNewComponent = true; - this.componentsStore.set( - componentName, - createComponent(newResource, this, path), - ); - } + const newType = (newResource.component?.state?.validSpec?.renderer ?? + (this.allowUnvalidatedSpec + ? newResource.component?.spec?.renderer + : undefined)) as CanvasComponentType; + const existingClass = + this.componentsStore.getNonReactive(componentName); + const path = constructPath(rowIndex, columnIndex, newType, prefix); + + if (existingClass && areSameType(newType, existingClass.type)) { + existingClass.update(newResource, path); + } else { + createdNewComponent = true; + this.componentsStore.set( + componentName, + createComponent(newResource, this, path), + ); + } + }); }); - }); + }; + + processRowItems(rows, ["rows"]); - const didUpdateRowCount = this._rows.updateFromCanvasRows(rows); + const didUpdateRowCount = this.processLayout(rows); existingKeys.difference(set).forEach((componentName) => { const component = this.componentsStore.getNonReactive(componentName); @@ -706,8 +740,113 @@ export class CanvasEntity { this.selectedComponent.update(($) => $); }; - generateId = (row: number | undefined, column: number | undefined) => { - return `${this.name}--component-${row ?? 0}-${column ?? 0}`; + // Build the ordered list of layout blocks (plain rows and tab groups) from the spec, + // and sync the underlying grids: _rows holds the top-level plain rows (in order), + // while each tab group owns a grid per tab. + private processLayout = (rows: V1CanvasRow[]): boolean => { + const freeRows: V1CanvasRow[] = []; + const blocks: LayoutBlock[] = []; + const seenGroupNames = new Set(); + + rows.forEach((row, rowIndex) => { + if (row.tabGroup) { + const name = row.tabGroup.name ?? `group-${rowIndex}`; + let group = this.tabGroups.get(name); + if (!group) { + group = new TabGroup(this, name); + this.tabGroups.set(name, group); + } + group.updateFromSpec(name, row.tabGroup.tabs ?? [], rowIndex); + seenGroupNames.add(name); + blocks.push({ kind: "tab-group", rowIndex, group }); + } else { + blocks.push({ kind: "row", rowIndex, freeRowIndex: freeRows.length }); + freeRows.push(row); + } + }); + + // Drop tab groups that no longer exist in the spec. + for (const name of [...this.tabGroups.keys()]) { + if (!seenGroupNames.has(name)) this.tabGroups.delete(name); + } + + const didUpdateRowCount = this._rows.updateFromCanvasRows(freeRows); + this.layout.set(blocks); + + // In view mode, the active tab per group is driven by the URL. (In the editor, + // allowUnvalidatedSpec is true and the active tab is editor-local state.) + if (!this.allowUnvalidatedSpec) { + this.applyTabsFromURL(); + } + + return didUpdateRowCount; + }; + + // Read the `tabs` URL param (group:tab pairs) and apply it to the matching tab groups. + // Groups absent from the param are reset to their first tab so back/forward navigation + // restores tab state symmetrically (a removed pair means "first tab"). + applyTabsFromURL = () => { + if (typeof window === "undefined") return; + const param = new URLSearchParams(window.location.search).get( + CANVAS_TABS_URL_PARAM, + ); + + const active = new Map(); + if (param) { + for (const pair of param.split(",")) { + const [groupName, tabName] = pair.split(":"); + // Each part is encoded on write so group/tab names containing ":" or "," round-trip. + if (groupName && tabName) { + active.set( + decodeURIComponent(groupName), + decodeURIComponent(tabName), + ); + } + } + } + + this.tabGroups.forEach((group, name) => { + const tabName = active.get(name); + if (tabName) group.setActiveByName(tabName); + else group.activeTabIndex.set(0); + }); + }; + + // Select a tab in a group and reflect every group's active tab in the URL. + // Groups left on their first tab are omitted to keep the URL short. + setActiveTabInURL = (groupName: string, tabName: string) => { + const group = this.tabGroups.get(groupName); + if (group) group.setActiveByName(tabName); + + if (typeof window === "undefined") return; + const params = new URLSearchParams(window.location.search); + const pairs: string[] = []; + this.tabGroups.forEach((g, name) => { + const index = get(g.activeTabIndex); + const tab = get(g.tabs)[index]; + // Encode each part so a group/tab name containing the ":" or "," delimiters survives. + if (tab && index > 0) { + pairs.push( + `${encodeURIComponent(name)}:${encodeURIComponent(tab.name)}`, + ); + } + }); + + if (pairs.length) params.set(CANVAS_TABS_URL_PARAM, pairs.join(",")); + else params.delete(CANVAS_TABS_URL_PARAM); + + goto(`?${params.toString()}`, { replaceState: true }).catch(console.error); + }; + + // namePrefix disambiguates components nested in tabs (e.g. "g2-t0-") so they don't collide + // with top-level components at the same row/col. It mirrors layout-util's generateId and the + // parser's position key (see parse_canvas.go). + generateId = ( + row: number | undefined, + column: number | undefined, + namePrefix = "", + ) => { + return `${this.name}--component-${namePrefix}${row ?? 0}-${column ?? 0}`; }; createOptimisticResource = (options: { @@ -717,8 +856,16 @@ export class CanvasEntity { metricsViewName: string; metricsViewSpec: V1MetricsViewSpec | undefined; spec?: ComponentSpec; + namePrefix?: string; }): V1Resource => { - const { type, row, column, metricsViewName, metricsViewSpec } = options; + const { + type, + row, + column, + metricsViewName, + metricsViewSpec, + namePrefix = "", + } = options; const spec = options.spec ?? @@ -730,7 +877,7 @@ export class CanvasEntity { return { meta: { name: { - name: this.generateId(row, column), + name: this.generateId(row, column, namePrefix), kind: ResourceKind.Component, }, }, @@ -777,20 +924,31 @@ export class CanvasEntity { }; } -export type ComponentPath = [ - "rows", - number, - "items", - number, - CanvasComponentType, -]; +// A YAML path to a component's renderer block. For a top-level row it looks like +// ["rows", row, "items", col, type]; for a row nested in a tab it is prefixed, e.g. +// ["rows", b, "tabs", t, "rows", row, "items", col, type] (the YAML omits the proto's +// tab_group wrapper). The path always ends with [..., "rows", row, "items", col, type], +// so row/col are read from the end. +export type ComponentPath = (string | number)[]; function constructPath( row: number, column: number, type: CanvasComponentType, + prefix: (string | number)[] = ["rows"], ): ComponentPath { - return ["rows", row, "items", column, type]; + return [...prefix, row, "items", column, type]; +} + +/** Extract the row and column indices from a component path, regardless of any tab prefix. */ +export function rowColFromPath(path: ComponentPath): { + row: number; + col: number; +} { + return { + row: Number(path.at(-4)), + col: Number(path.at(-2)), + }; } function areSameType( diff --git a/web-common/src/features/canvas/stores/tab-edit.spec.ts b/web-common/src/features/canvas/stores/tab-edit.spec.ts new file mode 100644 index 000000000000..67a93e188027 --- /dev/null +++ b/web-common/src/features/canvas/stores/tab-edit.spec.ts @@ -0,0 +1,273 @@ +import { describe, it, expect } from "vitest"; +import { parseDocument } from "yaml"; +import { + addTab, + addTabGroup, + addTabGroupAt, + convertRowToTabGroup, + deleteTab, + isTabGroupRow, + moveItemAcrossContainers, + moveTab, + renameTab, + reorderTab, + tabCount, + tabHasContent, +} from "./tab-edit"; + +const BASE = `type: canvas +rows: + - items: + - component: c1 +`; + +describe("tab-edit YAML transforms", () => { + it("addTabGroup appends a group with one empty tab", () => { + const doc = parseDocument(BASE); + const index = addTabGroup(doc); + + expect(index).toBe(1); + expect(isTabGroupRow(doc, 1)).toBe(true); + expect(tabCount(doc, 1)).toBe(1); + + const json = doc.toJSON(); + expect(json.rows[1]).toEqual({ + tabs: [{ label: "Tab 1", rows: [] }], + }); + // The pre-existing free row is untouched. + expect(json.rows[0]).toEqual({ items: [{ component: "c1" }] }); + }); + + it("addTabGroup creates the rows sequence when absent", () => { + const doc = parseDocument(`type: canvas\n`); + const index = addTabGroup(doc); + expect(index).toBe(0); + expect(doc.toJSON().rows).toHaveLength(1); + }); + + it("addTab appends a labeled empty tab", () => { + const doc = parseDocument(BASE); + addTabGroup(doc); + const tabIndex = addTab(doc, 1); + + expect(tabIndex).toBe(1); + expect(tabCount(doc, 1)).toBe(2); + expect(doc.toJSON().rows[1].tabs[1]).toEqual({ label: "Tab 2", rows: [] }); + }); + + it("addTab is a noop on a plain row", () => { + const doc = parseDocument(BASE); + expect(addTab(doc, 0)).toBe(-1); + }); + + it("renameTab updates the label", () => { + const doc = parseDocument(BASE); + addTabGroup(doc); + renameTab(doc, 1, 0, "Overview"); + expect(doc.toJSON().rows[1].tabs[0].label).toBe("Overview"); + }); + + it("deleteTab removes a tab when more than one remains", () => { + const doc = parseDocument(BASE); + addTabGroup(doc); + addTab(doc, 1); + expect(tabCount(doc, 1)).toBe(2); + + const result = deleteTab(doc, 1, 0); + expect(result).toBe("removed-tab"); + expect(tabCount(doc, 1)).toBe(1); + // The surviving tab is the one that was at index 1. + expect(doc.toJSON().rows[1].tabs[0].label).toBe("Tab 2"); + }); + + it("moveTab swaps a tab with its neighbor", () => { + const doc = parseDocument(BASE); + addTabGroup(doc); + addTab(doc, 1); + renameTab(doc, 1, 0, "First"); + renameTab(doc, 1, 1, "Second"); + + moveTab(doc, 1, 0, 1); + const labels = doc + .toJSON() + .rows[1].tabs.map((t: { label: string }) => t.label); + expect(labels).toEqual(["Second", "First"]); + }); + + it("moveTab is a noop at the boundary", () => { + const doc = parseDocument(BASE); + addTabGroup(doc); + addTab(doc, 1); + moveTab(doc, 1, 0, -1); // already leftmost + const labels = doc + .toJSON() + .rows[1].tabs.map((t: { label: string }) => t.label); + expect(labels).toEqual(["Tab 1", "Tab 2"]); + }); + + it("convertRowToTabGroup wraps a plain row as Tab 1", () => { + const doc = parseDocument(`type: canvas +rows: + - items: + - component: a + - component: b +`); + const ok = convertRowToTabGroup(doc, 0); + expect(ok).toBe(true); + expect(isTabGroupRow(doc, 0)).toBe(true); + + const json = doc.toJSON(); + expect(json.rows[0].tabs).toHaveLength(1); + expect(json.rows[0].tabs[0].label).toBe("Tab 1"); + expect(json.rows[0].tabs[0].rows).toEqual([ + { items: [{ component: "a" }, { component: "b" }] }, + ]); + }); + + it("convertRowToTabGroup is a noop on an existing tab group", () => { + const doc = parseDocument(BASE); + addTabGroup(doc); + expect(convertRowToTabGroup(doc, 1)).toBe(false); + }); + + it("deleteTab unwraps the group into free rows when deleting the last tab", () => { + const doc = parseDocument(`type: canvas +rows: + - items: + - component: header + - tabs: + - label: Only + rows: + - items: + - component: a + - items: + - component: b +`); + expect(isTabGroupRow(doc, 1)).toBe(true); + + const result = deleteTab(doc, 1, 0); + expect(result).toBe("unwrapped-group"); + + const json = doc.toJSON(); + // The group block is replaced by its tab's two rows, after the header row. + expect(json.rows).toHaveLength(3); + expect(json.rows[0]).toEqual({ items: [{ component: "header" }] }); + expect(json.rows[1]).toEqual({ items: [{ component: "a" }] }); + expect(json.rows[2]).toEqual({ items: [{ component: "b" }] }); + expect(isTabGroupRow(doc, 1)).toBe(false); + }); + + it("addTabGroupAt inserts a group at the given index", () => { + const doc = parseDocument(`type: canvas +rows: + - items: + - component: a + - items: + - component: b +`); + const index = addTabGroupAt(doc, 1); + expect(index).toBe(1); + + const json = doc.toJSON(); + expect(json.rows).toHaveLength(3); + expect(isTabGroupRow(doc, 1)).toBe(true); + expect(json.rows[0]).toEqual({ items: [{ component: "a" }] }); + expect(json.rows[2]).toEqual({ items: [{ component: "b" }] }); + }); + + it("reorderTab moves a tab from one position to another", () => { + const doc = parseDocument(`type: canvas +rows: + - tabs: + - label: A + rows: [] + - label: B + rows: [] + - label: C + rows: [] +`); + reorderTab(doc, 0, 0, 2); + + const labels = doc + .toJSON() + .rows[0].tabs.map((t: { label: string }) => t.label); + expect(labels).toEqual(["B", "C", "A"]); + }); + + it("moveItemAcrossContainers drops a widget to the left of a widget inside a tab", () => { + const doc = parseDocument(`type: canvas +rows: + - items: + - component: outside_a + - name: deep_dive + tabs: + - label: Overview + rows: + - items: + - component: inside_b +`); + const ok = moveItemAcrossContainers( + doc, + { rowsPath: ["rows"], row: 0, col: 0 }, + { rowsPath: ["rows", 1, "tabs", 0, "rows"], row: 0, col: 0 }, + ); + expect(ok).toBe(true); + + const json = doc.toJSON(); + // The source free row was removed; the tab group remains a tab group (no items key). + expect(json.rows).toHaveLength(1); + expect(json.rows[0].items).toBeUndefined(); + expect(json.rows[0].tabs).toBeDefined(); + // The widget joined the existing tab row to the LEFT of inside_b, losing nothing. + expect(json.rows[0].tabs[0].rows[0].items).toEqual([ + { component: "outside_a" }, + { component: "inside_b" }, + ]); + }); + + it("moveItemAcrossContainers appends a new row when not dropping into a column", () => { + const doc = parseDocument(`type: canvas +rows: + - name: g + tabs: + - label: A + rows: + - items: + - component: a + - label: B + rows: + - items: + - component: b +`); + // Move from tab A (row 0) to tab B as a new row (col null). + const ok = moveItemAcrossContainers( + doc, + { rowsPath: ["rows", 0, "tabs", 0, "rows"], row: 0, col: 0 }, + { rowsPath: ["rows", 0, "tabs", 1, "rows"], col: null }, + ); + expect(ok).toBe(true); + + const tabs = doc.toJSON().rows[0].tabs; + // Tab A is now empty; tab B has its original row plus the moved one. + expect(tabs[0].rows).toEqual([]); + expect(tabs[1].rows).toEqual([ + { items: [{ component: "b" }] }, + { items: [{ component: "a" }] }, + ]); + }); + + it("tabHasContent reflects whether a tab has rows", () => { + const doc = parseDocument(`type: canvas +rows: + - tabs: + - label: Empty + rows: [] + - label: Full + rows: + - items: + - component: a +`); + expect(tabHasContent(doc, 0, 0)).toBe(false); + expect(tabHasContent(doc, 0, 1)).toBe(true); + }); +}); diff --git a/web-common/src/features/canvas/stores/tab-edit.ts b/web-common/src/features/canvas/stores/tab-edit.ts new file mode 100644 index 000000000000..b5a4ad735741 --- /dev/null +++ b/web-common/src/features/canvas/stores/tab-edit.ts @@ -0,0 +1,224 @@ +import { isMap, isSeq, type Document } from "yaml"; + +// Pure YAML-document transforms for authoring tab groups in the visual editor. +// These operate on the parsed YAML Document (the editor's source of truth) and +// mutate it in place; the caller persists the result via the file artifact. +// +// The YAML shape of a tab group is a top-level rows entry with `tabs` (and an +// optional `name`), where each tab has a `label` and its own `rows`: +// +// rows: +// - name: # optional +// tabs: +// - label: Overview +// rows: [ ... ] +// +// This differs from the proto JSON shape (row.tabGroup.tabs); see canvasRowYAML +// in runtime/parser/parse_canvas.go. + +const MAX_ITEMS_PER_ROW = 4; + +/** + * Move a component item between two row containers (top-level rows or a tab's rows), + * identified by their YAML paths. Used for cross-container drags (e.g. dragging a widget + * from the free canvas into a tab). The item is removed from the source and inserted into + * the destination: + * - if `dest.col` is a number and `dest.row` points at an existing destination row with + * room, the item joins that row at that column (e.g. dropping to the left of a widget); + * - otherwise it becomes a new row inserted at `dest.row` (or appended). + * + * Node references for both containers are resolved up front, so removing the source row + * never invalidates the destination even when the source sits above a destination tab group. + * Returns true if the move was applied. + */ +export function moveItemAcrossContainers( + doc: Document, + source: { rowsPath: (string | number)[]; row: number; col: number }, + dest: { rowsPath: (string | number)[]; row?: number; col: number | null }, +): boolean { + const sourceSeq = doc.getIn(source.rowsPath); + const destSeq = doc.getIn(dest.rowsPath); + if (!isSeq(sourceSeq) || !isSeq(destSeq)) return false; + + const sourceRow = sourceSeq.items[source.row]; + if (!isMap(sourceRow)) return false; + const sourceItems = sourceRow.get("items"); + if (!isSeq(sourceItems)) return false; + const itemNode = sourceItems.items[source.col]; + if (itemNode === undefined) return false; + + // Decide the destination shape before mutating, so a full row falls back to a new row. + const destRowNode = + dest.row !== undefined ? destSeq.items[dest.row] : undefined; + const destRowItems = isMap(destRowNode) + ? destRowNode.get("items") + : undefined; + const joinExistingRow = + dest.col !== null && + isSeq(destRowItems) && + destRowItems.items.length < MAX_ITEMS_PER_ROW; + + // Remove from the source (and drop the row if it is now empty). + sourceItems.items.splice(source.col, 1); + if (sourceItems.items.length === 0) { + sourceSeq.items.splice(source.row, 1); + } + + if (joinExistingRow && isSeq(destRowItems)) { + const at = Math.min(dest.col as number, destRowItems.items.length); + destRowItems.items.splice(at, 0, itemNode); + } else { + const newRow = doc.createNode({ items: [itemNode] }); + const at = dest.row ?? destSeq.items.length; + destSeq.items.splice(Math.min(at, destSeq.items.length), 0, newRow); + } + + return true; +} + +/** Number of top-level entries in the rows sequence. */ +function rowCount(doc: Document): number { + const rows = doc.get("rows"); + return isSeq(rows) ? rows.items.length : 0; +} + +/** True if the top-level rows entry at the given index is a tab group. */ +export function isTabGroupRow(doc: Document, blockIndex: number): boolean { + const row = doc.getIn(["rows", blockIndex]); + return isMap(row) && row.has("tabs"); +} + +/** Number of tabs in the tab group at the given top-level index. */ +export function tabCount(doc: Document, blockIndex: number): number { + const tabs = doc.getIn(["rows", blockIndex, "tabs"]); + return isSeq(tabs) ? tabs.items.length : 0; +} + +/** True if the tab at [blockIndex, tabIndex] has at least one row of content. */ +export function tabHasContent( + doc: Document, + blockIndex: number, + tabIndex: number, +): boolean { + const rows = doc.getIn(["rows", blockIndex, "tabs", tabIndex, "rows"]); + return isSeq(rows) && rows.items.length > 0; +} + +/** + * Append a new tab group (with a single empty "Tab 1") at the end of the canvas. + * Returns the top-level index of the new group. + */ +export function addTabGroup(doc: Document): number { + return addTabGroupAt(doc, rowCount(doc)); +} + +/** + * Insert a new tab group (with a single empty "Tab 1") at the given top-level index. + * Returns the index at which it was inserted. + */ +export function addTabGroupAt(doc: Document, index: number): number { + const group = doc.createNode({ tabs: [{ label: "Tab 1", rows: [] }] }); + const rows = doc.get("rows"); + if (isSeq(rows)) { + const clamped = Math.max(0, Math.min(index, rows.items.length)); + rows.items.splice(clamped, 0, group); + return clamped; + } + doc.setIn(["rows"], doc.createNode([group])); + return 0; +} + +/** + * Append a new empty tab to the tab group at the given top-level index. + * Returns the index of the new tab, or -1 if the entry is not a tab group. + */ +export function addTab(doc: Document, blockIndex: number): number { + if (!isTabGroupRow(doc, blockIndex)) return -1; + const label = `Tab ${tabCount(doc, blockIndex) + 1}`; + doc.addIn(["rows", blockIndex, "tabs"], doc.createNode({ label, rows: [] })); + return tabCount(doc, blockIndex) - 1; +} + +/** Rename the tab at [blockIndex, tabIndex]. */ +export function renameTab( + doc: Document, + blockIndex: number, + tabIndex: number, + label: string, +): void { + if (!isTabGroupRow(doc, blockIndex)) return; + doc.setIn(["rows", blockIndex, "tabs", tabIndex, "label"], label); +} + +/** Move the tab at tabIndex one position in the given direction (-1 left, 1 right). */ +export function moveTab( + doc: Document, + blockIndex: number, + tabIndex: number, + direction: -1 | 1, +): void { + reorderTab(doc, blockIndex, tabIndex, tabIndex + direction); +} + +/** Move the tab at `from` to position `to` within its group (drag-to-reorder). */ +export function reorderTab( + doc: Document, + blockIndex: number, + from: number, + to: number, +): void { + if (!isTabGroupRow(doc, blockIndex)) return; + const tabs = doc.getIn(["rows", blockIndex, "tabs"]); + if (!isSeq(tabs)) return; + if (from === to || from < 0 || from >= tabs.items.length) return; + if (to < 0 || to >= tabs.items.length) return; + const [moved] = tabs.items.splice(from, 1); + tabs.items.splice(to, 0, moved); +} + +/** + * Wrap the plain row at rowIndex into a new single-tab tab group in place. The row's + * content becomes "Tab 1"'s only row. Returns true if the conversion happened. + */ +export function convertRowToTabGroup(doc: Document, rowIndex: number): boolean { + const rows = doc.get("rows"); + if (!isSeq(rows)) return false; + const row = doc.getIn(["rows", rowIndex]); + if (!isMap(row) || row.has("tabs")) return false; + + const group = doc.createNode({ + tabs: [{ label: "Tab 1", rows: [row.toJSON()] }], + }); + rows.items.splice(rowIndex, 1, group); + return true; +} + +/** + * Delete the tab at [blockIndex, tabIndex]. + * + * If it is the group's last remaining tab, the whole group is removed and that + * tab's rows are unwrapped back into free rows at the group's position, so no + * layout is lost. Returns the action taken. + */ +export function deleteTab( + doc: Document, + blockIndex: number, + tabIndex: number, +): "removed-tab" | "unwrapped-group" | "noop" { + if (!isTabGroupRow(doc, blockIndex)) return "noop"; + + if (tabCount(doc, blockIndex) > 1) { + doc.deleteIn(["rows", blockIndex, "tabs", tabIndex]); + return "removed-tab"; + } + + // Last tab (only index 0 remains): unwrap its rows into free rows at this position. + const rows = doc.get("rows"); + if (!isSeq(rows)) return "noop"; + + const tabRows = doc.getIn(["rows", blockIndex, "tabs", 0, "rows"]); + const unwrapped = isSeq(tabRows) ? tabRows.items : []; + rows.items.splice(blockIndex, 1, ...unwrapped); + + return "unwrapped-group"; +} diff --git a/web-common/src/features/canvas/stores/tab-group.ts b/web-common/src/features/canvas/stores/tab-group.ts new file mode 100644 index 000000000000..ab57f4820295 --- /dev/null +++ b/web-common/src/features/canvas/stores/tab-group.ts @@ -0,0 +1,122 @@ +import { get, writable } from "svelte/store"; +import type { + V1CanvasRow, + V1CanvasTab, +} from "@rilldata/web-common/runtime-client"; +import { Grid } from "./grid"; +import type { CanvasEntity } from "./canvas-entity"; + +/** + * A single tab within a tab group. Owns its own Grid (the tab's rows) and the + * YAML path prefix at which those rows live, so the existing component-path and + * transaction machinery can target a tab's rows the same way it targets top-level rows. + */ +export class Tab { + /** Stable identifier, derived from the label; used for URL state. */ + name: string; + /** User-facing label. */ + displayName: string; + /** The tab's rows. */ + grid: Grid; + /** YAML path prefix for this tab's rows, e.g. ["rows", 2, "tabs", 0, "rows"]. */ + yamlPathPrefix: (string | number)[]; + + constructor( + canvas: CanvasEntity, + name: string, + displayName: string, + yamlPathPrefix: (string | number)[], + ) { + this.name = name; + this.displayName = displayName; + this.yamlPathPrefix = yamlPathPrefix; + this.grid = new Grid(canvas); + } + + updateFromTab(tab: V1CanvasTab, yamlPathPrefix: (string | number)[]) { + this.name = tab.name ?? this.name; + this.displayName = tab.displayName ?? this.displayName; + this.yamlPathPrefix = yamlPathPrefix; + this.grid.updateFromCanvasRows(tab.rows ?? []); + } +} + +/** + * A tab group: a top-level layout block that renders a strip of tabs, only one of + * which is active (and mounted) at a time. + */ +export class TabGroup { + /** Stable identifier; used for URL state. */ + name: string; + /** Index of the active (mounted) tab. Editor-local while editing; URL-driven in view mode. */ + activeTabIndex = writable(0); + /** The tabs in this group. */ + tabs = writable([]); + + constructor( + private canvas: CanvasEntity, + name: string, + ) { + this.name = name; + } + + /** + * Sync the group's tabs from the spec. The blockIndex is the top-level row index + * at which this tab group sits, used to construct each tab's YAML path prefix. + */ + updateFromSpec(name: string, tabs: V1CanvasTab[], blockIndex: number) { + this.name = name; + const current = get(this.tabs); + + const next = tabs.map((tab, tabIndex) => { + // NOTE: this is the YAML path (row.tabs[t].rows), which differs from the + // proto JSON shape (row.tabGroup.tabs[t].rows). pathInYAML edits the YAML document. + const prefix = ["rows", blockIndex, "tabs", tabIndex, "rows"]; + const t = + current[tabIndex] ?? + new Tab( + this.canvas, + tab.name ?? `tab-${tabIndex}`, + tab.displayName ?? `Tab ${tabIndex + 1}`, + prefix, + ); + // Always sync from the spec — a newly-created tab must populate its grid too, + // otherwise its rows render empty until the next reprocess. + t.updateFromTab(tab, prefix); + return t; + }); + + this.tabs.set(next); + + // Clamp the active index if tabs were removed. + const activeIndex = get(this.activeTabIndex); + if (activeIndex >= next.length) { + this.activeTabIndex.set(Math.max(0, next.length - 1)); + } + } + + /** Select a tab by its stable name. Returns false if no such tab exists. */ + setActiveByName(name: string): boolean { + const index = get(this.tabs).findIndex((t) => t.name === name); + if (index === -1) return false; + this.activeTabIndex.set(index); + return true; + } + + getActiveTab(): Tab | undefined { + return get(this.tabs)[get(this.activeTabIndex)]; + } +} + +/** + * A top-level layout block. The canvas body is an ordered list of these: each is + * either a plain row or a tab group, mirroring the heterogeneous `rows` array in the spec. + */ +export type LayoutBlock = + | { kind: "row"; rowIndex: number; freeRowIndex: number } + | { kind: "tab-group"; rowIndex: number; group: TabGroup }; + +/** True if the spec contains any tab groups. */ +export function specHasTabGroups(rows: V1CanvasRow[] | undefined): boolean { + return !!rows?.some((row) => !!row.tabGroup); +} diff --git a/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts b/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts index 448692e8484b..a92213096e9f 100644 --- a/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts +++ b/web-common/src/proto/gen/rill/runtime/v1/resources_pb.ts @@ -5191,12 +5191,20 @@ export class CanvasRow extends Message { heightUnit = ""; /** - * Items to render in the row. + * Items to render in the row. Empty when the row is a tab group. * * @generated from field: repeated rill.runtime.v1.CanvasItem items = 3; */ items: CanvasItem[] = []; + /** + * If set, this row renders a tab group instead of items. + * A row has either items or a tab_group, never both. + * + * @generated from field: rill.runtime.v1.CanvasTabGroup tab_group = 4; + */ + tabGroup?: CanvasTabGroup; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -5208,6 +5216,7 @@ export class CanvasRow extends Message { { no: 1, name: "height", kind: "scalar", T: 13 /* ScalarType.UINT32 */, opt: true }, { no: 2, name: "height_unit", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 3, name: "items", kind: "message", T: CanvasItem, repeated: true }, + { no: 4, name: "tab_group", kind: "message", T: CanvasTabGroup }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): CanvasRow { @@ -5227,6 +5236,110 @@ export class CanvasRow extends Message { } } +/** + * @generated from message rill.runtime.v1.CanvasTabGroup + */ +export class CanvasTabGroup extends Message { + /** + * Stable identifier for the tab group, used for URL state. + * Defaults to "group-" if not provided in the canvas YAML. + * + * @generated from field: string name = 1; + */ + name = ""; + + /** + * Tabs in the group. A group always has at least one tab. + * + * @generated from field: repeated rill.runtime.v1.CanvasTab tabs = 2; + */ + tabs: CanvasTab[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "rill.runtime.v1.CanvasTabGroup"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "tabs", kind: "message", T: CanvasTab, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CanvasTabGroup { + return new CanvasTabGroup().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CanvasTabGroup { + return new CanvasTabGroup().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CanvasTabGroup { + return new CanvasTabGroup().fromJsonString(jsonString, options); + } + + static equals(a: CanvasTabGroup | PlainMessage | undefined, b: CanvasTabGroup | PlainMessage | undefined): boolean { + return proto3.util.equals(CanvasTabGroup, a, b); + } +} + +/** + * @generated from message rill.runtime.v1.CanvasTab + */ +export class CanvasTab extends Message { + /** + * Stable identifier for the tab, used for URL state. Derived from the label. + * + * @generated from field: string name = 1; + */ + name = ""; + + /** + * User-facing label for the tab. + * + * @generated from field: string display_name = 2; + */ + displayName = ""; + + /** + * Rows to render when the tab is active. These are always plain rows; + * a tab's rows never contain a nested tab_group. + * + * @generated from field: repeated rill.runtime.v1.CanvasRow rows = 3; + */ + rows: CanvasRow[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "rill.runtime.v1.CanvasTab"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "display_name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "rows", kind: "message", T: CanvasRow, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): CanvasTab { + return new CanvasTab().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): CanvasTab { + return new CanvasTab().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): CanvasTab { + return new CanvasTab().fromJsonString(jsonString, options); + } + + static equals(a: CanvasTab | PlainMessage | undefined, b: CanvasTab | PlainMessage | undefined): boolean { + return proto3.util.equals(CanvasTab, a, b); + } +} + /** * @generated from message rill.runtime.v1.CanvasItem */ diff --git a/web-common/src/runtime-client/gen/index.schemas.ts b/web-common/src/runtime-client/gen/index.schemas.ts index 1728e4d538cf..2d39e538d159 100644 --- a/web-common/src/runtime-client/gen/index.schemas.ts +++ b/web-common/src/runtime-client/gen/index.schemas.ts @@ -440,13 +440,32 @@ If not found in `time_ranges`, it should be added to the list. */ filterExpr?: V1CanvasPresetFilterExpr; } +export interface V1CanvasTab { + /** Stable identifier for the tab, used for URL state. Derived from the label. */ + name?: string; + /** User-facing label for the tab. */ + displayName?: string; + /** Rows to render when the tab is active. These are always plain rows; +a tab's rows never contain a nested tab_group. */ + rows?: V1CanvasRow[]; +} + +export interface V1CanvasTabGroup { + /** Stable identifier for the tab group, used for URL state. +Defaults to "group-" if not provided in the canvas YAML. */ + name?: string; + /** Tabs in the group. A group always has at least one tab. */ + tabs?: V1CanvasTab[]; +} + export interface V1CanvasRow { /** Height of the row. The unit is given in height_unit. */ height?: number; /** Unit of the height. Current possible values: "px", empty string. */ heightUnit?: string; - /** Items to render in the row. */ + /** Items to render in the row. Empty when the row is a tab group. */ items?: V1CanvasItem[]; + tabGroup?: V1CanvasTabGroup; } export interface V1CanvasSpec { diff --git a/web-common/tests/web-admin-client.mock.ts b/web-common/tests/web-admin-client.mock.ts new file mode 100644 index 000000000000..3065bc81e7f6 --- /dev/null +++ b/web-common/tests/web-admin-client.mock.ts @@ -0,0 +1,6 @@ +// Test-mode stub for the admin client. canvas-entity dynamically imports +// `@rilldata/web-admin/client` only in the cloud context; web-common unit tests cannot +// resolve that package, so this mock satisfies the import graph. See vite.config.ts. +export function getAdminServiceListBookmarksQueryOptions() { + return {}; +} diff --git a/web-common/vite.config.ts b/web-common/vite.config.ts index 65efdf8efee0..4ffc724c71af 100644 --- a/web-common/vite.config.ts +++ b/web-common/vite.config.ts @@ -20,6 +20,12 @@ export default defineConfig(({ mode }) => { find: "$app/environment", replacement: "/../web-common/tests/app-environment.mock.ts", }); + // canvas-entity dynamically imports the admin client only in the cloud context; stub + // it so web-common unit tests that pull in canvas-entity can resolve the import graph. + alias.push({ + find: "@rilldata/web-admin/client", + replacement: "/../web-common/tests/web-admin-client.mock.ts", + }); } return { From c32f590508a236e0fa4c69e0371cded3635ac898 Mon Sep 17 00:00:00 2001 From: Dhiraj Kumar Date: Tue, 23 Jun 2026 16:31:06 +0530 Subject: [PATCH 02/24] fix misagligned tab icons, convert to menu --- .../src/features/canvas/CanvasBuilder.svelte | 7 - .../src/features/canvas/CanvasTabStrip.svelte | 136 +++++++++--------- .../canvas/EditableCanvasTabGroup.svelte | 7 - 3 files changed, 72 insertions(+), 78 deletions(-) diff --git a/web-common/src/features/canvas/CanvasBuilder.svelte b/web-common/src/features/canvas/CanvasBuilder.svelte index 5792a322cbe2..ff89fb6a1d65 100644 --- a/web-common/src/features/canvas/CanvasBuilder.svelte +++ b/web-common/src/features/canvas/CanvasBuilder.svelte @@ -45,7 +45,6 @@ moveTab, moveItemAcrossContainers, renameTab, - reorderTab, tabHasContent, } from "./stores/tab-edit"; import { getCanvasStore } from "./state-managers/state-managers"; @@ -477,11 +476,6 @@ updateContents(); } - function reorderTabAction(blockIndex: number, from: number, to: number) { - reorderTab(contents, blockIndex, from, to); - updateContents(); - } - function convertRowToTabGroupAction(rowIndex: number) { if (convertRowToTabGroup(contents, rowIndex)) updateContents(); } @@ -790,7 +784,6 @@ onRenameTab={renameTabAction} onDeleteTab={deleteTabAction} onMoveTab={moveTabAction} - onReorderTab={reorderTabAction} onDropOnTab={dropComponentOnTab} onAddTabGroup={addTabGroupAtAction} /> diff --git a/web-common/src/features/canvas/CanvasTabStrip.svelte b/web-common/src/features/canvas/CanvasTabStrip.svelte index 172caeab4803..5689bf4d0d7d 100644 --- a/web-common/src/features/canvas/CanvasTabStrip.svelte +++ b/web-common/src/features/canvas/CanvasTabStrip.svelte @@ -1,6 +1,8 @@
{#each $tabs as tab, index (tab.name)} -