diff --git a/docs/docs/reference/project-files/canvas-dashboards.md b/docs/docs/reference/project-files/canvas-dashboards.md index b7637aff9677..10aa49d6fd7d 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,16 @@ _[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 shown on the tab. + + - **`name`** - _[string]_ - Stable identifier used as the tab's deep-link URL key. Defaults to a slug of the label if omitted. + + - **`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 2b52b687b8b7..d5840fa90148 100644 --- a/proto/gen/rill/runtime/v1/resources.pb.go +++ b/proto/gen/rill/runtime/v1/resources.pb.go @@ -5915,8 +5915,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() { @@ -5972,6 +5975,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 @@ -5990,7 +6125,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) } @@ -6003,7 +6138,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 { @@ -6016,7 +6151,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 { @@ -6070,7 +6205,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) } @@ -6083,7 +6218,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 { @@ -6096,7 +6231,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 { @@ -6138,7 +6273,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) } @@ -6151,7 +6286,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 { @@ -6164,7 +6299,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 { @@ -6187,7 +6322,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) } @@ -6200,7 +6335,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 { @@ -6213,7 +6348,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 { @@ -6249,7 +6384,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) } @@ -6262,7 +6397,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 { @@ -6275,7 +6410,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 { @@ -6350,7 +6485,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) } @@ -6363,7 +6498,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 { @@ -6376,7 +6511,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 { @@ -6394,7 +6529,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) } @@ -6407,7 +6542,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 { @@ -6420,7 +6555,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 { @@ -6473,7 +6608,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) } @@ -6486,7 +6621,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 { @@ -6499,7 +6634,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 { @@ -6549,7 +6684,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) } @@ -6562,7 +6697,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 { @@ -6575,7 +6710,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 { @@ -6604,7 +6739,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) } @@ -6617,7 +6752,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 { @@ -6630,7 +6765,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 { @@ -6658,7 +6793,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) } @@ -6671,7 +6806,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 { @@ -6684,7 +6819,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 { @@ -6705,7 +6840,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) } @@ -6718,7 +6853,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 { @@ -6731,7 +6866,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 { @@ -6753,7 +6888,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) } @@ -6766,7 +6901,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 { @@ -6779,7 +6914,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 { @@ -6811,7 +6946,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) } @@ -6824,7 +6959,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 { @@ -6837,7 +6972,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 { @@ -6886,7 +7021,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) } @@ -6899,7 +7034,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 { @@ -6912,7 +7047,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 { @@ -6952,7 +7087,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) } @@ -6965,7 +7100,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 { @@ -7100,7 +7235,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) } @@ -7113,7 +7248,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 { @@ -7167,7 +7302,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) } @@ -7180,7 +7315,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 { @@ -7247,7 +7382,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) } @@ -7260,7 +7395,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 { @@ -7428,7 +7563,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) } @@ -7441,7 +7576,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 { @@ -7558,7 +7693,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) } @@ -7571,7 +7706,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 { @@ -9109,7 +9244,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, @@ -9117,226 +9252,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 ( @@ -9352,7 +9503,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, 85) +var file_rill_runtime_v1_resources_proto_msgTypes = make([]protoimpl.MessageInfo, 87) var file_rill_runtime_v1_resources_proto_goTypes = []any{ (ReconcileStatus)(0), // 0: rill.runtime.v1.ReconcileStatus (ModelChangeMode)(0), // 1: rill.runtime.v1.ModelChangeMode @@ -9420,42 +9571,44 @@ 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.CanvasSpec.AnnotationsEntry - nil, // 92: rill.runtime.v1.CanvasPreset.FilterExprEntry - (*timestamppb.Timestamp)(nil), // 93: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 94: google.protobuf.Struct - (*StructType)(nil), // 95: rill.runtime.v1.StructType - (TimeGrain)(0), // 96: rill.runtime.v1.TimeGrain - (*Expression)(nil), // 97: rill.runtime.v1.Expression - (ExportFormat)(0), // 98: rill.runtime.v1.ExportFormat - (*Color)(nil), // 99: rill.runtime.v1.Color - (*structpb.Value)(nil), // 100: google.protobuf.Value - (*Type)(nil), // 101: 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.CanvasSpec.AnnotationsEntry + nil, // 94: rill.runtime.v1.CanvasPreset.FilterExprEntry + (*timestamppb.Timestamp)(nil), // 95: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 96: google.protobuf.Struct + (*StructType)(nil), // 97: rill.runtime.v1.StructType + (TimeGrain)(0), // 98: rill.runtime.v1.TimeGrain + (*Expression)(nil), // 99: rill.runtime.v1.Expression + (ExportFormat)(0), // 100: rill.runtime.v1.ExportFormat + (*Color)(nil), // 101: rill.runtime.v1.Color + (*structpb.Value)(nil), // 102: google.protobuf.Value + (*Type)(nil), // 103: 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 @@ -9471,53 +9624,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 - 93, // 18: rill.runtime.v1.ResourceMeta.created_on:type_name -> google.protobuf.Timestamp - 93, // 19: rill.runtime.v1.ResourceMeta.spec_updated_on:type_name -> google.protobuf.Timestamp - 93, // 20: rill.runtime.v1.ResourceMeta.state_updated_on:type_name -> google.protobuf.Timestamp - 93, // 21: rill.runtime.v1.ResourceMeta.deleted_on:type_name -> google.protobuf.Timestamp + 95, // 18: rill.runtime.v1.ResourceMeta.created_on:type_name -> google.protobuf.Timestamp + 95, // 19: rill.runtime.v1.ResourceMeta.spec_updated_on:type_name -> google.protobuf.Timestamp + 95, // 20: rill.runtime.v1.ResourceMeta.state_updated_on:type_name -> google.protobuf.Timestamp + 95, // 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 - 93, // 23: rill.runtime.v1.ResourceMeta.reconcile_on:type_name -> google.protobuf.Timestamp + 95, // 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 - 93, // 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 + 95, // 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 - 94, // 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 - 93, // 33: rill.runtime.v1.SourceState.refreshed_on:type_name -> google.protobuf.Timestamp + 96, // 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 + 95, // 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 - 94, // 37: rill.runtime.v1.ModelSpec.incremental_state_resolver_properties:type_name -> google.protobuf.Struct - 94, // 38: rill.runtime.v1.ModelSpec.partitions_resolver_properties:type_name -> google.protobuf.Struct - 94, // 39: rill.runtime.v1.ModelSpec.input_properties:type_name -> google.protobuf.Struct - 94, // 40: rill.runtime.v1.ModelSpec.stage_properties:type_name -> google.protobuf.Struct - 94, // 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 + 96, // 37: rill.runtime.v1.ModelSpec.incremental_state_resolver_properties:type_name -> google.protobuf.Struct + 96, // 38: rill.runtime.v1.ModelSpec.partitions_resolver_properties:type_name -> google.protobuf.Struct + 96, // 39: rill.runtime.v1.ModelSpec.input_properties:type_name -> google.protobuf.Struct + 96, // 40: rill.runtime.v1.ModelSpec.stage_properties:type_name -> google.protobuf.Struct + 96, // 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 - 94, // 44: rill.runtime.v1.ModelState.result_properties:type_name -> google.protobuf.Struct - 93, // 45: rill.runtime.v1.ModelState.refreshed_on:type_name -> google.protobuf.Timestamp - 94, // 46: rill.runtime.v1.ModelState.incremental_state:type_name -> google.protobuf.Struct - 95, // 47: rill.runtime.v1.ModelState.incremental_state_schema:type_name -> rill.runtime.v1.StructType - 94, // 48: rill.runtime.v1.ModelTest.resolver_properties:type_name -> google.protobuf.Struct + 96, // 44: rill.runtime.v1.ModelState.result_properties:type_name -> google.protobuf.Struct + 95, // 45: rill.runtime.v1.ModelState.refreshed_on:type_name -> google.protobuf.Timestamp + 96, // 46: rill.runtime.v1.ModelState.incremental_state:type_name -> google.protobuf.Struct + 97, // 47: rill.runtime.v1.ModelState.incremental_state_schema:type_name -> rill.runtime.v1.StructType + 96, // 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 - 96, // 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 + 98, // 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 @@ -9525,10 +9678,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 - 97, // 67: rill.runtime.v1.SecurityRuleRowFilter.expression:type_name -> rill.runtime.v1.Expression + 99, // 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 - 93, // 70: rill.runtime.v1.MetricsViewState.data_refreshed_on:type_name -> google.protobuf.Timestamp + 95, // 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 @@ -9538,11 +9691,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 - 93, // 80: rill.runtime.v1.ExploreState.data_refreshed_on:type_name -> google.protobuf.Timestamp + 95, // 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 - 97, // 84: rill.runtime.v1.ExplorePreset.where:type_name -> rill.runtime.v1.Expression + 99, // 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 @@ -9551,99 +9704,102 @@ 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 - 94, // 94: rill.runtime.v1.ReportSpec.resolver_properties:type_name -> google.protobuf.Struct - 98, // 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 + 96, // 94: rill.runtime.v1.ReportSpec.resolver_properties:type_name -> google.protobuf.Struct + 100, // 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 - 93, // 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 + 95, // 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 - 93, // 101: rill.runtime.v1.ReportExecution.report_time:type_name -> google.protobuf.Timestamp - 93, // 102: rill.runtime.v1.ReportExecution.started_on:type_name -> google.protobuf.Timestamp - 93, // 103: rill.runtime.v1.ReportExecution.finished_on:type_name -> google.protobuf.Timestamp + 95, // 101: rill.runtime.v1.ReportExecution.report_time:type_name -> google.protobuf.Timestamp + 95, // 102: rill.runtime.v1.ReportExecution.started_on:type_name -> google.protobuf.Timestamp + 95, // 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 - 94, // 107: rill.runtime.v1.AlertSpec.resolver_properties:type_name -> google.protobuf.Struct - 94, // 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 + 96, // 107: rill.runtime.v1.AlertSpec.resolver_properties:type_name -> google.protobuf.Struct + 96, // 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 - 94, // 111: rill.runtime.v1.Notifier.properties:type_name -> google.protobuf.Struct - 93, // 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 + 96, // 111: rill.runtime.v1.Notifier.properties:type_name -> google.protobuf.Struct + 95, // 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 - 93, // 116: rill.runtime.v1.AlertExecution.execution_time:type_name -> google.protobuf.Timestamp - 93, // 117: rill.runtime.v1.AlertExecution.started_on:type_name -> google.protobuf.Timestamp - 93, // 118: rill.runtime.v1.AlertExecution.finished_on:type_name -> google.protobuf.Timestamp - 93, // 119: rill.runtime.v1.AlertExecution.suppressed_since:type_name -> google.protobuf.Timestamp + 95, // 116: rill.runtime.v1.AlertExecution.execution_time:type_name -> google.protobuf.Timestamp + 95, // 117: rill.runtime.v1.AlertExecution.started_on:type_name -> google.protobuf.Timestamp + 95, // 118: rill.runtime.v1.AlertExecution.finished_on:type_name -> google.protobuf.Timestamp + 95, // 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 - 94, // 121: rill.runtime.v1.AssertionResult.fail_row:type_name -> google.protobuf.Struct + 96, // 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 - 99, // 128: rill.runtime.v1.ThemeSpec.primary_color:type_name -> rill.runtime.v1.Color - 99, // 129: rill.runtime.v1.ThemeSpec.secondary_color:type_name -> rill.runtime.v1.Color + 101, // 128: rill.runtime.v1.ThemeSpec.primary_color:type_name -> rill.runtime.v1.Color + 101, // 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 - 94, // 135: rill.runtime.v1.ComponentSpec.renderer_properties:type_name -> google.protobuf.Struct + 96, // 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 - 93, // 139: rill.runtime.v1.ComponentState.data_refreshed_on:type_name -> google.protobuf.Timestamp - 100, // 140: rill.runtime.v1.ComponentVariable.default_value:type_name -> google.protobuf.Value + 95, // 139: rill.runtime.v1.ComponentState.data_refreshed_on:type_name -> google.protobuf.Timestamp + 102, // 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 - 91, // 149: rill.runtime.v1.CanvasSpec.annotations:type_name -> rill.runtime.v1.CanvasSpec.AnnotationsEntry + 93, // 149: rill.runtime.v1.CanvasSpec.annotations:type_name -> rill.runtime.v1.CanvasSpec.AnnotationsEntry 63, // 150: rill.runtime.v1.CanvasState.valid_spec:type_name -> rill.runtime.v1.CanvasSpec - 93, // 151: rill.runtime.v1.CanvasState.data_refreshed_on:type_name -> google.protobuf.Timestamp - 66, // 152: rill.runtime.v1.CanvasRow.items:type_name -> rill.runtime.v1.CanvasItem - 2, // 153: rill.runtime.v1.CanvasPreset.comparison_mode:type_name -> rill.runtime.v1.ExploreComparisonMode - 92, // 154: rill.runtime.v1.CanvasPreset.filter_expr:type_name -> rill.runtime.v1.CanvasPreset.FilterExprEntry - 97, // 155: rill.runtime.v1.DefaultMetricsSQLFilter.expression:type_name -> rill.runtime.v1.Expression - 70, // 156: rill.runtime.v1.API.spec:type_name -> rill.runtime.v1.APISpec - 71, // 157: rill.runtime.v1.API.state:type_name -> rill.runtime.v1.APIState - 94, // 158: rill.runtime.v1.APISpec.resolver_properties:type_name -> google.protobuf.Struct - 23, // 159: rill.runtime.v1.APISpec.security_rules:type_name -> rill.runtime.v1.SecurityRule - 77, // 160: rill.runtime.v1.ParseError.start_location:type_name -> rill.runtime.v1.CharLocation - 79, // 161: rill.runtime.v1.ConnectorV2.spec:type_name -> rill.runtime.v1.ConnectorSpec - 80, // 162: rill.runtime.v1.ConnectorV2.state:type_name -> rill.runtime.v1.ConnectorState - 94, // 163: rill.runtime.v1.ConnectorSpec.properties:type_name -> google.protobuf.Struct - 94, // 164: rill.runtime.v1.ConnectorSpec.provision_args:type_name -> google.protobuf.Struct - 6, // 165: rill.runtime.v1.MetricsViewSpec.Dimension.type:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionType - 96, // 166: rill.runtime.v1.MetricsViewSpec.Dimension.smallest_time_grain:type_name -> rill.runtime.v1.TimeGrain - 101, // 167: rill.runtime.v1.MetricsViewSpec.Dimension.data_type:type_name -> rill.runtime.v1.Type - 96, // 168: rill.runtime.v1.MetricsViewSpec.DimensionSelector.time_grain:type_name -> rill.runtime.v1.TimeGrain - 82, // 169: rill.runtime.v1.MetricsViewSpec.MeasureWindow.order_by:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector - 7, // 170: rill.runtime.v1.MetricsViewSpec.Measure.type:type_name -> rill.runtime.v1.MetricsViewSpec.MeasureType - 83, // 171: rill.runtime.v1.MetricsViewSpec.Measure.window:type_name -> rill.runtime.v1.MetricsViewSpec.MeasureWindow - 82, // 172: rill.runtime.v1.MetricsViewSpec.Measure.per_dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector - 82, // 173: rill.runtime.v1.MetricsViewSpec.Measure.required_dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector - 94, // 174: rill.runtime.v1.MetricsViewSpec.Measure.format_d3_locale:type_name -> google.protobuf.Struct - 101, // 175: rill.runtime.v1.MetricsViewSpec.Measure.data_type:type_name -> rill.runtime.v1.Type - 35, // 176: rill.runtime.v1.MetricsViewSpec.Annotation.measures_selector:type_name -> rill.runtime.v1.FieldSelector - 96, // 177: rill.runtime.v1.MetricsViewSpec.Rollup.time_grain:type_name -> rill.runtime.v1.TimeGrain - 35, // 178: rill.runtime.v1.MetricsViewSpec.Rollup.dimensions_selector:type_name -> rill.runtime.v1.FieldSelector - 35, // 179: rill.runtime.v1.MetricsViewSpec.Rollup.measures_selector:type_name -> rill.runtime.v1.FieldSelector - 68, // 180: rill.runtime.v1.CanvasPreset.FilterExprEntry.value:type_name -> rill.runtime.v1.DefaultMetricsSQLFilter - 181, // [181:181] is the sub-list for method output_type - 181, // [181:181] is the sub-list for method input_type - 181, // [181:181] is the sub-list for extension type_name - 181, // [181:181] is the sub-list for extension extendee - 0, // [0:181] is the sub-list for field type_name + 95, // 151: rill.runtime.v1.CanvasState.data_refreshed_on:type_name -> google.protobuf.Timestamp + 68, // 152: rill.runtime.v1.CanvasRow.items:type_name -> rill.runtime.v1.CanvasItem + 66, // 153: rill.runtime.v1.CanvasRow.tab_group:type_name -> rill.runtime.v1.CanvasTabGroup + 67, // 154: rill.runtime.v1.CanvasTabGroup.tabs:type_name -> rill.runtime.v1.CanvasTab + 65, // 155: rill.runtime.v1.CanvasTab.rows:type_name -> rill.runtime.v1.CanvasRow + 2, // 156: rill.runtime.v1.CanvasPreset.comparison_mode:type_name -> rill.runtime.v1.ExploreComparisonMode + 94, // 157: rill.runtime.v1.CanvasPreset.filter_expr:type_name -> rill.runtime.v1.CanvasPreset.FilterExprEntry + 99, // 158: rill.runtime.v1.DefaultMetricsSQLFilter.expression:type_name -> rill.runtime.v1.Expression + 72, // 159: rill.runtime.v1.API.spec:type_name -> rill.runtime.v1.APISpec + 73, // 160: rill.runtime.v1.API.state:type_name -> rill.runtime.v1.APIState + 96, // 161: rill.runtime.v1.APISpec.resolver_properties:type_name -> google.protobuf.Struct + 23, // 162: rill.runtime.v1.APISpec.security_rules:type_name -> rill.runtime.v1.SecurityRule + 79, // 163: rill.runtime.v1.ParseError.start_location:type_name -> rill.runtime.v1.CharLocation + 81, // 164: rill.runtime.v1.ConnectorV2.spec:type_name -> rill.runtime.v1.ConnectorSpec + 82, // 165: rill.runtime.v1.ConnectorV2.state:type_name -> rill.runtime.v1.ConnectorState + 96, // 166: rill.runtime.v1.ConnectorSpec.properties:type_name -> google.protobuf.Struct + 96, // 167: rill.runtime.v1.ConnectorSpec.provision_args:type_name -> google.protobuf.Struct + 6, // 168: rill.runtime.v1.MetricsViewSpec.Dimension.type:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionType + 98, // 169: rill.runtime.v1.MetricsViewSpec.Dimension.smallest_time_grain:type_name -> rill.runtime.v1.TimeGrain + 103, // 170: rill.runtime.v1.MetricsViewSpec.Dimension.data_type:type_name -> rill.runtime.v1.Type + 98, // 171: rill.runtime.v1.MetricsViewSpec.DimensionSelector.time_grain:type_name -> rill.runtime.v1.TimeGrain + 84, // 172: rill.runtime.v1.MetricsViewSpec.MeasureWindow.order_by:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector + 7, // 173: rill.runtime.v1.MetricsViewSpec.Measure.type:type_name -> rill.runtime.v1.MetricsViewSpec.MeasureType + 85, // 174: rill.runtime.v1.MetricsViewSpec.Measure.window:type_name -> rill.runtime.v1.MetricsViewSpec.MeasureWindow + 84, // 175: rill.runtime.v1.MetricsViewSpec.Measure.per_dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector + 84, // 176: rill.runtime.v1.MetricsViewSpec.Measure.required_dimensions:type_name -> rill.runtime.v1.MetricsViewSpec.DimensionSelector + 96, // 177: rill.runtime.v1.MetricsViewSpec.Measure.format_d3_locale:type_name -> google.protobuf.Struct + 103, // 178: rill.runtime.v1.MetricsViewSpec.Measure.data_type:type_name -> rill.runtime.v1.Type + 35, // 179: rill.runtime.v1.MetricsViewSpec.Annotation.measures_selector:type_name -> rill.runtime.v1.FieldSelector + 98, // 180: rill.runtime.v1.MetricsViewSpec.Rollup.time_grain:type_name -> rill.runtime.v1.TimeGrain + 35, // 181: rill.runtime.v1.MetricsViewSpec.Rollup.dimensions_selector:type_name -> rill.runtime.v1.FieldSelector + 35, // 182: rill.runtime.v1.MetricsViewSpec.Rollup.measures_selector:type_name -> rill.runtime.v1.FieldSelector + 70, // 183: rill.runtime.v1.CanvasPreset.FilterExprEntry.value:type_name -> rill.runtime.v1.DefaultMetricsSQLFilter + 184, // [184:184] is the sub-list for method output_type + 184, // [184:184] is the sub-list for method input_type + 184, // [184:184] is the sub-list for extension type_name + 184, // [184:184] is the sub-list for extension extendee + 0, // [0:184] is the sub-list for field type_name } func init() { file_rill_runtime_v1_resources_proto_init() } @@ -10354,7 +10510,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: @@ -10366,7 +10522,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: @@ -10378,7 +10534,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: @@ -10390,7 +10546,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: @@ -10402,7 +10558,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: @@ -10414,7 +10570,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: @@ -10426,7 +10582,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: @@ -10438,7 +10594,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: @@ -10450,7 +10606,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: @@ -10462,7 +10618,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: @@ -10474,7 +10630,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: @@ -10486,7 +10642,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: @@ -10498,7 +10654,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: @@ -10510,7 +10666,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: @@ -10522,7 +10678,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: @@ -10534,7 +10690,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: @@ -10546,7 +10702,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: @@ -10558,7 +10714,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: @@ -10570,7 +10726,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: @@ -10582,7 +10738,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: @@ -10594,6 +10750,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 @@ -10645,15 +10825,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: 85, + NumMessages: 87, 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 724ad1f0af02..7c1b6cc5759e 100644 --- a/proto/gen/rill/runtime/v1/resources.pb.validate.go +++ b/proto/gen/rill/runtime/v1/resources.pb.validate.go @@ -10716,6 +10716,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 } @@ -10797,6 +10826,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 ceec8e20a45d..b9d6c9a6930f 100644 --- a/proto/gen/rill/runtime/v1/runtime.swagger.yaml +++ b/proto/gen/rill/runtime/v1/runtime.swagger.yaml @@ -4416,7 +4416,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: @@ -4517,6 +4522,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 8e776f1e9b4b..62f2d4590256 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 9dd9800f83aa..135f940ffc21 100644 --- a/runtime/parser/parse_canvas.go +++ b/runtime/parser/parse_canvas.go @@ -40,17 +40,36 @@ type CanvasYAML struct { ComparisonDimension string `yaml:"comparison_dimension"` 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"` - Annotations map[string]string `yaml:"annotations"` + Variables []*ComponentVariableYAML `yaml:"variables"` + Rows []*canvasRowYAML `yaml:"rows"` + Security *SecurityPolicyYAML `yaml:"security"` + Annotations map[string]string `yaml:"annotations"` +} + +// 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. `label` is the display name shown on the +// tab; `name` is the stable URL key (defaulted from the label when omitted). +type canvasTabYAML struct { + Name string `yaml:"name"` + 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 { @@ -135,84 +154,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 @@ -308,12 +254,207 @@ 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) + } + + // Use the explicit name as the URL key when provided, otherwise derive one from the + // label. Either way uniquify it against earlier tabs in this group. + var tabName string + if tab.Name != "" { + tabName = uniqueName(tab.Name, fmt.Sprintf("tab-%d", t), seenNames) + } else { + 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{} @@ -329,11 +470,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..7482f6f9a7b7 100644 --- a/runtime/parser/parser_test.go +++ b/runtime/parser/parser_test.go @@ -1885,6 +1885,262 @@ 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) +} + +// TestCanvasTabExplicitName verifies a tab can carry an explicit URL `name` alongside its +// display `label`, and that `name` defaults from the label when omitted. +func TestCanvasTabExplicitName(t *testing.T) { + ctx := context.Background() + repo := makeRepo(t, map[string]string{ + `rill.yaml`: ``, + `canvases/d1.yaml`: ` +type: canvas +rows: +- tabs: + - name: ov + label: Overview + rows: [] + - label: Deep Dive + 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 + // Explicit name + label. + require.Equal(t, "ov", tabs[0].Name) + require.Equal(t, "Overview", tabs[0].DisplayName) + // Name defaults from the label. + require.Equal(t, "deep-dive", tabs[1].Name) + require.Equal(t, "Deep Dive", tabs[1].DisplayName) +} + 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 89bed1132379..3d041c5bfd33 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,25 @@ 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 shown on the tab. + name: + type: string + description: Stable identifier used as the tab's deep-link URL key. Defaults to a slug of the label if omitted. + 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..bb77379b07fe --- /dev/null +++ b/web-common/src/features/canvas/CanvasTabStrip.svelte @@ -0,0 +1,338 @@ + + + + + + diff --git a/web-common/src/features/canvas/EditableCanvasRow.svelte b/web-common/src/features/canvas/EditableCanvasRow.svelte index 9a13483ec856..78132dafdcfe 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,10 @@ 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 = ""; + export let zoneScope = "canvas"; let rowHeight = get(row.height) ?? MIN_HEIGHT; let hasLocalChange = false; @@ -66,7 +75,7 @@ $: updateHeightFromSpec($height); $: updateWidthsFromSpec($itemWidths); - $: id = `canvas-row-${rowIndex}`; + $: id = `canvas-row-${idPrefix}${rowIndex}`; function onRowResizeStart() { initialMousePosition = $mousePosition; @@ -146,8 +155,7 @@ const width = widths[columnIndex]; initialHeight = - document.querySelector(`#canvas-row-${rowIndex}`)?.getBoundingClientRect() - .height ?? + document.querySelector(`#${id}`)?.getBoundingClientRect().height ?? rowHeight ?? MIN_HEIGHT; @@ -193,6 +201,7 @@ {rowIndex} resizeIndex={-1} addIndex={columnIndex} + {zoneScope} rowLength={itemCount} dragging={activelyDragging} {isSpreadEvenly} @@ -205,6 +214,7 @@ {isSpreadEvenly} columnWidth={widths[columnIndex]} {rowIndex} + {zoneScope} dragging={activelyDragging} resizeIndex={columnIndex} addIndex={columnIndex + 1} @@ -219,7 +229,11 @@ row={rowIndex} maxColumns={itemCount} allowDrop={activelyDragging && - (itemCount < 4 || dragComponent?.pathInYAML?.[1] === rowIndex)} + (itemCount < 4 || + (dragComponent + ? rowColFromPath(dragComponent.pathInYAML).row === rowIndex + : false))} + {zoneScope} {onDrop} /> @@ -242,6 +256,9 @@ onDelete={() => { onDelete({ component }); }} + onConvertToTabGroup={onConvertToTabGroup + ? () => onConvertToTabGroup?.() + : undefined} /> {:else} @@ -252,21 +269,27 @@ allowDrop={activelyDragging} resizeIndex={rowIndex} dropIndex={rowIndex + 1} + {zoneScope} {onRowResizeStart} {onDrop} addItem={(type) => { initializeRow(rowIndex + 1, type); }} + onAddTabGroup={onAddTabGroup + ? () => onAddTabGroup?.(rowIndex + 1) + : undefined} /> {#if rowIndex === 0} { 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..e6ce76831550 --- /dev/null +++ b/web-common/src/features/canvas/EditableCanvasTabGroup.svelte @@ -0,0 +1,250 @@ + + + + +
+ onAddTab(blockIndex)} + onRenameTab={(tabIndex, label) => + onRenameTab(blockIndex, tabIndex, label)} + onDeleteTab={(tabIndex) => onDeleteTab(blockIndex, tabIndex)} + onMoveTab={(tabIndex, direction) => + onMoveTab(blockIndex, tabIndex, direction)} + onDuplicateTab={(tabIndex) => onDuplicateTab(blockIndex, tabIndex)} + {onSelectGroup} + 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 $grid.length === 0} + + + {#if hasValidMetrics} + + + {:else} + + {/if} + + + {/if} + {/if} +
+
+ + { + initializeRow(blockIndex, type); + }} + onAddTabGroup={() => onAddTabGroup(blockIndex)} + /> + + { + initializeRow(blockIndex + 1, type); + }} + onAddTabGroup={() => onAddTabGroup(blockIndex + 1)} + /> +
+ +{#if isLastBlock && hasValidMetrics} + + + + initializeRow(blockIndex + 1, type)} + onAddTabGroup={() => onAddTabGroup(blockIndex + 1)} + /> + + +{/if} + + diff --git a/web-common/src/features/canvas/ElementDivider.svelte b/web-common/src/features/canvas/ElementDivider.svelte index adbacb56ed09..20c56851bc4c 100644 --- a/web-common/src/features/canvas/ElementDivider.svelte +++ b/web-common/src/features/canvas/ElementDivider.svelte @@ -11,6 +11,7 @@ export let addIndex: number; export let rowLength: number; export let rowIndex: number; + export let zoneScope = "canvas"; export let columnWidth: number | undefined = undefined; export let isSpreadEvenly: boolean; export let dragging: boolean; @@ -27,11 +28,11 @@ $: firstElement = addIndex === 0; $: lastElement = addIndex === rowLength; - $: dividerId = `row:${rowIndex}::column:${resizeIndex}`; + $: dividerId = `${zoneScope}::row:${rowIndex}::column:${resizeIndex}`; $: isActiveDivider = $activeDivider === dividerId; - $: dropId = `row:${rowIndex}::column:${addIndex}`; + $: dropId = `${zoneScope}::row:${rowIndex}::column:${addIndex}`; $: isDropZone = $dropZone === dropId; $: notActiveDivider = !isActiveDivider && !!$activeDivider; diff --git a/web-common/src/features/canvas/ItemWrapper.svelte b/web-common/src/features/canvas/ItemWrapper.svelte index 28e52a53d853..954598db5be2 100644 --- a/web-common/src/features/canvas/ItemWrapper.svelte +++ b/web-common/src/features/canvas/ItemWrapper.svelte @@ -3,13 +3,22 @@ export let zIndex: number; export let type: string | undefined = undefined; + // When true, the wrapper sizes to its content rather than the type's fixed + // initial height. Used for the "add widget" filler so it doesn't reserve a + // full row's worth of empty space below the button. + export let fitContent = false; $: expandable = type === "kpi_grid" || type === "markdown" || type === "leaderboard"; $: minHeight = getInitialHeight(type) + "px"; -
+
@@ -20,11 +29,15 @@ container-name: component-container; } - .expandable { + .fit-content { + @apply h-fit; + } + + .expandable:not(.fit-content) { min-height: var(--row-height); } - :not(.expandable) { + :not(.expandable):not(.fit-content) { height: max(var(--row-height), var(--min-height)); min-height: 100%; } diff --git a/web-common/src/features/canvas/RowDropZone.svelte b/web-common/src/features/canvas/RowDropZone.svelte index 01748761ac1f..e090e764074a 100644 --- a/web-common/src/features/canvas/RowDropZone.svelte +++ b/web-common/src/features/canvas/RowDropZone.svelte @@ -2,35 +2,40 @@ import AddComponentDropdown from "./AddComponentDropdown.svelte"; import type { CanvasComponentType } from "./components/types"; import Divider from "./Divider.svelte"; - import { dropZone, activeDivider } from "./stores/ui-stores"; + import { activeDivider, dropZone } from "./stores/ui-stores"; export let allowDrop: boolean; export let resizeIndex = -1; export let dropIndex: number; + export let zoneScope = "canvas"; + export let position: "top" | "bottom" | undefined = undefined; 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; - $: dividerId = `row:${resizeIndex}::column:null`; - $: dropId = `row:${dropIndex}::column:null`; + $: notResizable = resizeIndex === -1; + $: resolvedPosition = position ?? (notResizable ? "top" : "bottom"); + + $: dividerId = `${zoneScope}::resize-row:${resizeIndex}::drop-row:${dropIndex}::column:null::position:${resolvedPosition}`; + $: dropId = `${zoneScope}::row:${dropIndex}::column:null`; $: isDropZone = $dropZone === dropId; $: isActiveDivider = $activeDivider === dividerId; $: notActiveDivider = !isActiveDivider && !!$activeDivider; - $: notResizable = resizeIndex === -1; - $: forceShowDivider = menuOpen || isActiveDivider || isDropZone; @@ -99,7 +105,7 @@ } */ .top { - @apply top-0 -translate-y-1/2; + @apply top-0 -translate-y-1/2; } .bottom { 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/DropZone.svelte b/web-common/src/features/canvas/components/DropZone.svelte index 7788e5190198..8ebf4906c400 100644 --- a/web-common/src/features/canvas/components/DropZone.svelte +++ b/web-common/src/features/canvas/components/DropZone.svelte @@ -3,6 +3,7 @@ export let column: number; export let row: number; + export let zoneScope = "canvas"; export let allowDrop: boolean; export let maxColumns: number; export let onDrop: (row: number, column: number) => void; @@ -10,7 +11,7 @@ {#each { length: 2 } as _, i (i)} {@const effectiveColumn = column + i} - {@const dropId = `row:${row}::column:${effectiveColumn}`} + {@const dropId = `${zoneScope}::row:${row}::column:${effectiveColumn}`}
+ 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/inspector/TabGroupEditor.svelte b/web-common/src/features/canvas/inspector/TabGroupEditor.svelte new file mode 100644 index 000000000000..c4fb1c376aaf --- /dev/null +++ b/web-common/src/features/canvas/inspector/TabGroupEditor.svelte @@ -0,0 +1,251 @@ + + + +
+ +
+ +
+ Tabs +
    + {#each $tabs as tab, index (index)} + 0} + canMoveDown={index < tabCount - 1} + onCommitLabel={(value) => commitLabel(index, value)} + onInputLabel={(value) => group.setTabDisplayName(index, value)} + onCommitName={(value) => commitName(index, value)} + onMoveUp={() => move(index, -1)} + onMoveDown={() => move(index, 1)} + onDuplicate={() => duplicate(index)} + onDelete={() => requestDeleteTab(index)} + /> + {/each} +
+
+ +
+ +
+
+ +{#if pendingTabDelete !== null} + {@const index = pendingTabDelete} + !open && (pendingTabDelete = null)} + > + + Delete tab? + + This tab and all of its widgets will be permanently removed. + + + + {#snippet child({ props })} + + {/snippet} + + + {#snippet child({ props })} + + {/snippet} + + + + +{/if} + +{#if pendingGroupDelete} + (pendingGroupDelete = open)}> + + Delete tab group? + + This tab group and all of its tabs and widgets will be permanently + removed. + + + + {#snippet child({ props })} + + {/snippet} + + + {#snippet child({ props })} + + {/snippet} + + + + +{/if} + + diff --git a/web-common/src/features/canvas/inspector/TabListItem.svelte b/web-common/src/features/canvas/inspector/TabListItem.svelte new file mode 100644 index 000000000000..f6f2df395626 --- /dev/null +++ b/web-common/src/features/canvas/inspector/TabListItem.svelte @@ -0,0 +1,102 @@ + + +
  • +
    + onCommitName(nameValue)} + onEnter={() => onCommitName(nameValue)} + /> + onInputLabel(value)} + onBlur={() => onCommitLabel(label)} + onEnter={() => onCommitLabel(label)} + /> +
    + +
    + + + + +
    +
  • + + diff --git a/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte b/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte index 124563470f15..1ca1ea07cb18 100644 --- a/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte +++ b/web-common/src/features/canvas/inspector/VisualCanvasEditing.svelte @@ -2,6 +2,7 @@ import { replaceState } from "$app/navigation"; import ComponentsEditor from "@rilldata/web-common/features/canvas/inspector/ComponentsEditor.svelte"; import PageEditor from "@rilldata/web-common/features/canvas/inspector/PageEditor.svelte"; + import TabGroupEditor from "@rilldata/web-common/features/canvas/inspector/TabGroupEditor.svelte"; import { getCanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; import type { FileArtifact } from "@rilldata/web-common/features/entity-management/file-artifact"; import { Inspector } from "@rilldata/web-common/layout/workspace"; @@ -17,9 +18,25 @@ $: ({ instanceId } = runtimeClient); $: ({ - canvasEntity: { selectedComponent, componentsStore }, + canvasEntity: { + selectedComponent, + componentsStore, + selectedTabGroup, + setSelectedTabGroup, + layout, + }, } = getCanvasStore(canvasName, instanceId)); + // Resolve the selected tab group to its layout block (for the active group + block index). + $: tabGroupBlock = + $selectedTabGroup != null + ? $layout.find( + (block) => + block.kind === "tab-group" && + block.group.name === $selectedTabGroup, + ) + : undefined; + $: ({ editorContent, updateEditorContent, saveLocalContent, path } = fileArtifact); @@ -73,6 +90,15 @@ > {#if component} + {:else if tabGroupBlock && tabGroupBlock.kind === "tab-group"} + setSelectedTabGroup(null)} + onRename={(name) => setSelectedTabGroup(name)} + /> {:else} {/if} 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/state-managers/state-managers.ts b/web-common/src/features/canvas/state-managers/state-managers.ts index 832dcbc595b4..3dec97d24f2a 100644 --- a/web-common/src/features/canvas/state-managers/state-managers.ts +++ b/web-common/src/features/canvas/state-managers/state-managers.ts @@ -21,10 +21,22 @@ function makeCanvasId(canvasName: string, instanceId: string): CanvasId { export function getCanvasStoreUnguarded( canvasName: string, instanceId: string, + allowUnvalidatedSpec?: boolean, ): CanvasStore | undefined { const id = makeCanvasId(canvasName, instanceId); + const store = canvasRegistry.get(id); + + if ( + store && + allowUnvalidatedSpec !== undefined && + store.canvasEntity.allowUnvalidatedSpec !== allowUnvalidatedSpec + ) { + store.canvasEntity.unsubscribe(); + canvasRegistry.delete(id); + return undefined; + } - return canvasRegistry.get(id); + return store; } export function getCanvasStore( @@ -65,11 +77,18 @@ export function setCanvasStore( ): CanvasStore { const id = makeCanvasId(canvasName, instanceId); - if (canvasRegistry.has(id)) { + const existingStore = canvasRegistry.get(id); + if ( + existingStore && + existingStore.canvasEntity.allowUnvalidatedSpec !== allowUnvalidatedSpec + ) { + existingStore.canvasEntity.unsubscribe(); + canvasRegistry.delete(id); + } else if (existingStore) { console.warn( `Canvas store for ID ${id} already exists. Returning existing store.`, ); - return canvasRegistry.get(id)!; + return existingStore; } const canvasEntity = new CanvasEntity( diff --git a/web-common/src/features/canvas/stores/canvas-entity.ts b/web-common/src/features/canvas/stores/canvas-entity.ts index 9d7b274f45e6..9d3256ff304e 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,30 @@ 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 `tabgroup_name.tab_name` +// references, e.g. `?tabs=deep_dive.detail,financials.costs`. +export const CANVAS_TABS_URL_PARAM = "tabs"; + +// Encode a group or tab name for the `tabs` URL param. encodeURIComponent already escapes the +// "," pair delimiter; we additionally escape "." so a name can't be confused with the +// "group.tab" separator. decodeURIComponent restores both on read. +function encodeTabKey(name: string): string { + return encodeURIComponent(name).replace(/\./g, "%2E"); +} + // 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 +81,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; @@ -82,6 +100,9 @@ export class CanvasEntity { selectedComponent = writable(null); activeComponent = writable(null); + // Name of the tab group currently selected for editing (drives the tab-group inspector + // panel). Mutually exclusive with selectedComponent. + selectedTabGroup = writable(null); parsedContent: Readable>; public specStore: CanvasSpecResponseStore; // Tracks whether the canvas been loaded (and rows processed) for the first time @@ -510,6 +531,7 @@ export class CanvasEntity { this.searchParams.set(searchParams); this.saveSnapshot(searchParams.toString()); this.timeManager.state.onUrlChange(searchParams); + this.applyTabsFromURL(); }; // Resubscribes to the spec store. Internal call to processSpec will recreate the components. @@ -618,8 +640,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); @@ -644,6 +668,7 @@ export class CanvasEntity { column: columnIndex, metricsViewName, metricsViewSpec, + namePrefix: namePrefixFromPath(pathInYAML), }); const newComponent = createComponent(newResource, this, path); @@ -659,50 +684,71 @@ 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; - // Tear down the replaced instance's spec subscription before - // overwriting it, otherwise the orphan keeps mutating shared - // filter/time state. - existingClass?.destroy(); - 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; + // Tear down the replaced instance's spec subscription before + // overwriting it, otherwise the orphan keeps mutating shared + // filter/time state. + existingClass?.destroy(); + 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); @@ -722,8 +768,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, spec updates follow the URL. In the editor, URL changes are + // applied in onUrlChange so spec churn from YAML edits does not reset local tab 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(",")) { + // Split on the first "." into group and tab; both parts are encoded on write so any + // "." / "," in a name is escaped and won't be mistaken for a delimiter. + const sep = pair.indexOf("."); + if (sep === -1) continue; + const groupName = decodeURIComponent(pair.slice(0, sep)); + const tabName = decodeURIComponent(pair.slice(sep + 1)); + if (groupName && tabName) active.set(groupName, tabName); + } + } + + this.tabGroups.forEach((group, name) => { + const tabName = active.get(name); + if (tabName) group.setActiveByName(tabName); + // In view mode, a group absent from the param is reset to its first tab so back/forward + // restores state symmetrically. In edit mode the active tab is editor-local (driven by + // clicks), so don't reset it here — doing so on every URL change fought direct selection. + else if (!this.allowUnvalidatedSpec) 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]; + // Reference the active tab as `tabgroup_name.tab_name`, encoding each part. + if (tab && index > 0) { + pairs.push(`${encodeTabKey(name)}.${encodeTabKey(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: { @@ -733,8 +884,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 ?? @@ -746,7 +905,7 @@ export class CanvasEntity { return { meta: { name: { - name: this.generateId(row, column), + name: this.generateId(row, column, namePrefix), kind: ResourceKind.Component, }, }, @@ -767,19 +926,36 @@ export class CanvasEntity { }; }; + // Inspector inputs (component title/description, tab name/display name) commit their value + // on blur. The elements that change the selection (canvas components, tab strip) are not + // focusable in a way that blurs the input first, so the pending edit would otherwise be lost + // or applied to the newly-selected target. Blurring the active element synchronously runs the + // input's onBlur — which writes the edit to the editor content — before the selection changes + // or the inspector panel unmounts. This is a single commit point for all blur-committed inputs. + private commitPendingInspectorEdit = () => { + if (typeof document === "undefined") return; + const active = document.activeElement; + if (active instanceof HTMLElement) active.blur(); + }; + setSelectedComponent = (id: string | null) => { - // Inspector inputs commit their value on blur. Canvas component elements are - // not focusable, so clicking another component never blurs the focused input, - // which would otherwise apply the pending edit to the newly-selected component. - // Blur the active element first so the edit commits against the component that - // is still selected, before we switch. - if (id !== get(this.selectedComponent) && typeof document !== "undefined") { - const active = document.activeElement; - if (active instanceof HTMLElement) active.blur(); - } + if (id !== get(this.selectedComponent)) this.commitPendingInspectorEdit(); + // Selecting a component takes over the inspector from any selected tab group. + if (id) this.selectedTabGroup.set(null); this.selectedComponent.set(id); }; + // Select a tab group for editing (opens the tab-group inspector panel). Clears any + // selected component so the two never fight over the inspector. + setSelectedTabGroup = (name: string | null) => { + if (name !== get(this.selectedTabGroup)) this.commitPendingInspectorEdit(); + if (name) this.selectedComponent.set(null); + this.selectedTabGroup.set(name); + }; + + // Look up a tab group by its stable name (for the inspector panel). + getTabGroup = (name: string) => this.tabGroups.get(name); + setActiveComponent = (id: string) => { this.activeComponent.set(id); }; @@ -794,20 +970,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..02ff5d8f1b17 --- /dev/null +++ b/web-common/src/features/canvas/stores/tab-edit.spec.ts @@ -0,0 +1,348 @@ +import { describe, it, expect } from "vitest"; +import { parseDocument } from "yaml"; +import { + addTab, + addTabGroup, + addTabGroupAt, + convertRowToTabGroup, + deleteTab, + deleteTabGroup, + duplicateTab, + isTabGroupRow, + moveItemAcrossContainers, + moveTab, + renameTab, + renameTabGroup, + reorderTab, + setTabName, + tabCount, + tabHasContent, +} from "./tab-edit"; + +const BASE = `type: canvas +rows: + - items: + - component: c1 +`; + +describe("tab-edit YAML transforms", () => { + it("duplicateTab inserts a copy after the original with a (copy) label", () => { + const doc = parseDocument(`type: canvas +rows: + - tabs: + - name: ov + label: Overview + rows: + - items: + - component: a + - label: Detail + rows: [] +`); + const newIndex = duplicateTab(doc, 0, 0); + expect(newIndex).toBe(1); + + const tabs = doc.toJSON().rows[0].tabs; + expect(tabs).toHaveLength(3); + expect(tabs[1].label).toBe("Overview (copy)"); + // The copy carries the original's rows/content but not its URL name. + expect(tabs[1].rows).toEqual([{ items: [{ component: "a" }] }]); + expect(tabs[1].name).toBeUndefined(); + // The original and the tab that followed it are preserved and in order. + expect(tabs[0].label).toBe("Overview"); + expect(tabs[2].label).toBe("Detail"); + }); + + it("renameTabGroup sets and clears the group name", () => { + const doc = parseDocument(`type: canvas +rows: + - name: deep_dive + tabs: + - label: A + rows: [] +`); + renameTabGroup(doc, 0, "Sales overview"); + expect(doc.toJSON().rows[0].name).toBe("Sales overview"); + + // Clearing the name removes the key so the parser can default it. + renameTabGroup(doc, 0, " "); + expect("name" in doc.toJSON().rows[0]).toBe(false); + }); + + it("deleteTabGroup removes the whole group entry", () => { + const doc = parseDocument(`type: canvas +rows: + - items: + - component: header + - tabs: + - label: A + rows: + - items: + - component: a +`); + expect(deleteTabGroup(doc, 1)).toBe(true); + + const json = doc.toJSON(); + expect(json.rows).toHaveLength(1); + expect(json.rows[0]).toEqual({ items: [{ component: "header" }] }); + // Deleting a non-tab-group entry is a no-op. + expect(deleteTabGroup(doc, 0)).toBe(false); + }); + + 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("setTabName sets and clears the tab URL name", () => { + const doc = parseDocument(BASE); + addTabGroup(doc); + setTabName(doc, 1, 0, "overview"); + expect(doc.toJSON().rows[1].tabs[0].name).toBe("overview"); + setTabName(doc, 1, 0, " "); + expect("name" in doc.toJSON().rows[1].tabs[0]).toBe(false); + }); + + 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..0066d4bc74c5 --- /dev/null +++ b/web-common/src/features/canvas/stores/tab-edit.ts @@ -0,0 +1,307 @@ +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` (display), an optional `name` +// (URL key, defaulted from the label), and its own `rows`: +// +// rows: +// - name: # optional +// tabs: +// - name: overview # optional URL key +// label: Overview # display label +// 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; +} + +/** + * Set (or clear) the tab group's `name` at the given top-level index. The name is the + * group's stable URL key; clearing it lets the parser fall back to `group-`. + */ +export function renameTabGroup( + doc: Document, + blockIndex: number, + name: string, +): void { + if (!isTabGroupRow(doc, blockIndex)) return; + const trimmed = name.trim(); + if (trimmed) { + doc.setIn(["rows", blockIndex, "name"], trimmed); + } else { + doc.deleteIn(["rows", blockIndex, "name"]); + } +} + +/** Set the display label of 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); +} + +/** + * Set (or clear) the URL `name` of the tab at [blockIndex, tabIndex]. Clearing it lets the + * parser re-derive the key from the label. + */ +export function setTabName( + doc: Document, + blockIndex: number, + tabIndex: number, + name: string, +): void { + if (!isTabGroupRow(doc, blockIndex)) return; + const trimmed = name.trim(); + if (trimmed) { + doc.setIn(["rows", blockIndex, "tabs", tabIndex, "name"], trimmed); + } else { + doc.deleteIn(["rows", blockIndex, "tabs", tabIndex, "name"]); + } +} + +/** 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); +} + +/** + * Duplicate the tab at [blockIndex, tabIndex], inserting the copy immediately after it. + * The copy's label gets a " (copy)" suffix and its `name` is dropped so the parser derives a + * fresh URL key; inline component definitions are re-derived to fresh names too (they are + * position-keyed). Returns the index of the new tab, or -1 if the entry is not a tab group. + */ +export function duplicateTab( + doc: Document, + blockIndex: number, + tabIndex: number, +): number { + if (!isTabGroupRow(doc, blockIndex)) return -1; + const tabs = doc.getIn(["rows", blockIndex, "tabs"]); + if (!isSeq(tabs)) return -1; + const original = tabs.items[tabIndex]; + if (!isMap(original)) return -1; + + // Drop the name so the copy gets a fresh URL key; carry everything else (rows, etc.). + const { name, label, ...rest } = original.toJSON() as { + name?: string; + label?: string; + [key: string]: unknown; + }; + void name; + const clone = doc.createNode({ + label: `${label ?? "Tab"} (copy)`, + ...rest, + }); + tabs.items.splice(tabIndex + 1, 0, clone); + return tabIndex + 1; +} + +/** + * Delete the entire tab group (and all of its tabs/components) at the given top-level index. + * Returns true if a tab group was removed. + */ +export function deleteTabGroup(doc: Document, blockIndex: number): boolean { + if (!isTabGroupRow(doc, blockIndex)) return false; + const rows = doc.get("rows"); + if (!isSeq(rows)) return false; + rows.items.splice(blockIndex, 1); + return true; +} + +/** + * 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..5b67332a3a5a --- /dev/null +++ b/web-common/src/features/canvas/stores/tab-group.ts @@ -0,0 +1,173 @@ +import type { + V1CanvasRow, + V1CanvasTab, +} from "@rilldata/web-common/runtime-client"; +import { get, writable } from "svelte/store"; +import type { CanvasEntity } from "./canvas-entity"; +import { Grid } from "./grid"; + +/** + * 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. + * The active tab is tracked by its name across spec rebuilds (see updateFromSpec), so this + * index always points at the same tab after a reorder. */ + activeTabIndex = writable(0); + /** The tabs in this group. */ + tabs = writable([]); + /* A tab index to activate as soon as it exists in the spec (used after add/duplicate, where + the new tab's name isn't known on the client until the spec reflects it). */ + private pendingActiveTabIndex: number | null = null; + + 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); + // The active tab is tracked by name: capture it before the rebuild so we can re-point the + // index at the same tab afterwards, no matter how the order changed. + const activeName = current[get(this.activeTabIndex)]?.name; + + // Match existing Tab instances by their stable name, not by index. On reorder a tab keeps + // its own grid and component instances and simply moves to a new index; matching by index + // would instead repurpose the object at each slot, leaving stale content (a tab showing + // its neighbour's widgets) until a full reload. + const byName = new Map(current.map((t) => [t.name, t])); + + 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 tabName = tab.name ?? `tab-${tabIndex}`; + const t = + byName.get(tabName) ?? + new Tab( + this.canvas, + tabName, + 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); + + if ( + this.pendingActiveTabIndex !== null && + this.pendingActiveTabIndex < next.length + ) { + // A just-added/duplicated tab: activate it by its (now-known) index. + this.activeTabIndex.set(this.pendingActiveTabIndex); + this.pendingActiveTabIndex = null; + } else if (activeName !== undefined) { + // Keep the same tab active across the rebuild. If it still exists (reorder), follow it to + // its new index; if it was renamed (name changed) or removed, the index is left as-is and + // clamped below, which keeps the active position stable for an in-place rename. + const index = next.findIndex((t) => t.name === activeName); + if (index !== -1) this.activeTabIndex.set(index); + } + + // 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)); + } + } + + /** + * Request that the tab at the given index become active once it appears in the spec. + * Used after adding/duplicating a tab, since the spec reprocess is async. + */ + activateWhenReady(index: number) { + this.pendingActiveTabIndex = index; + } + + /** + * Optimistically update a tab's display name so the strip reflects edits as the user types, + * before the YAML change is saved and reconciled. The committed value is reconciled later + * via updateFromSpec. + */ + setTabDisplayName(index: number, displayName: string) { + const list = get(this.tabs); + const tab = list[index]; + if (!tab || tab.displayName === displayName) return; + tab.displayName = displayName; + this.tabs.set([...list]); + } + + /** 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 9e46845336b3..750501a3d94f 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 @@ -5197,12 +5197,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); @@ -5214,6 +5222,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 { @@ -5233,6 +5242,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 77b9d65b1af1..2cb87bbc3997 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 {