Repository: vmware-tanzu/octant
Pull request #2173 (2021-03-18 05:31:19)

Bump google.golang.org/grpc from 1.35.0 to 1.36.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.35.0 to 1.36.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's releases</a>.</em></p>
<blockquote>
<h2>Release 1.36.0</h2>
<h1>New Features</h1>
<ul>
<li>xds bootstrap: support config content in env variable (<a href="https://github.com/grpc/grpc-go/issues/4153">#4153</a>)</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>encoding/proto: do not panic when types do not match (<a href="https://github.com/grpc/grpc-go/issues/4218">#4218</a>)</li>
</ul>
<h1>Documentation</h1>
<ul>
<li>status: document nil error handling of FromError (<a href="https://github.com/grpc/grpc-go/issues/4196">#4196</a>)
<ul>
<li>Special Thanks: <a href="https://github.com/gauravgahlot"><code>@​gauravgahlot</code></a></li>
</ul>
</li>
</ul>
<h2>Release 1.35.1</h2>
<ul>
<li>encoding/proto: do not panic when types do not match (<a href="https://github.com/grpc/grpc-go/issues/4218">#4218</a>)</li>
<li>xds: add env var protection for client-side security (<a href="https://github.com/grpc/grpc-go/issues/4247">#4247</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/grpc/grpc-go/commit/f74f0337644653eba7923908a4d7f79a4f3a267b"><code>f74f033</code></a> Change version to 1.36.0 (<a href="https://github.com/grpc/grpc-go/issues/4198">#4198</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/e6d71adde2d32e404ff1ab7558fd7e2ae00874ba"><code>e6d71ad</code></a> encoding/proto: do not panic when types do not match (<a href="https://github.com/grpc/grpc-go/issues/4218">#4218</a>) (<a href="https://github.com/grpc/grpc-go/issues/4223">#4223</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/61962d0e8e4ee228bd2ec73e02b19f90785310d0"><code>61962d0</code></a> status: document nil error handling of FromError (<a href="https://github.com/grpc/grpc-go/issues/4196">#4196</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/9280052d36656451dd7568a18a836c2a74edaf6c"><code>9280052</code></a> balancergroup: Propagate balancer.BuildOptions to child policies (<a href="https://github.com/grpc/grpc-go/issues/4184">#4184</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/b753f4903c1b09ced50ae223b439a771aff798f9"><code>b753f49</code></a> xds testing: increase timeout from 90 minutes to 120 minutes (<a href="https://github.com/grpc/grpc-go/issues/4191">#4191</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/7b8d65a7bc4446f61cceb8ee679847c100a07695"><code>7b8d65a</code></a> xds: rename internal/client files to remove client prefix (<a href="https://github.com/grpc/grpc-go/issues/4188">#4188</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/f005af03c24d49a4ec379884392cc4a557a0d047"><code>f005af0</code></a> examples: delete profiling example since profiling support was rolled back (#...</li>
<li><a href="https://github.com/grpc/grpc-go/commit/0bc741730b8171fc51cdaf826caea5119c411009"><code>0bc7417</code></a> xds: report drops by circuit breaking (<a href="https://github.com/grpc/grpc-go/issues/4171">#4171</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/e526a29227471c90813ef4d5c1b2df98e627bb28"><code>e526a29</code></a> xds: Remove v3Support environment variable (<a href="https://github.com/grpc/grpc-go/issues/4174">#4174</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/7bb497f78432890caa760b51fcfc077498b1fee2"><code>7bb497f</code></a> grpc: Update protobuf regenrate script (<a href="https://github.com/grpc/grpc-go/issues/4177">#4177</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/grpc/grpc-go/compare/v1.35.0...v1.36.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/grpc&package-manager=go_modules&previous-version=1.35.0&new-version=1.36.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/changelogs/unreleased/2165-GuessWhoSamFoo b/changelogs/unreleased/2165-GuessWhoSamFoo
new file mode 100644
index 00000000..a0357151
--- /dev/null
+++ b/changelogs/unreleased/2165-GuessWhoSamFoo
@@ -0,0 +1,1 @@
+Allow plugins to add multiple tabs
diff --git a/go.mod b/go.mod
index 5676dae3..1634d13a 100644
--- a/go.mod
+++ b/go.mod
@@ -29,7 +29,7 @@ require (
 	github.com/hashicorp/go-plugin v0.0.0-20190220160451-3f118e8ee104
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
-	github.com/iancoleman/strcase v0.1.2
+	github.com/iancoleman/strcase v0.1.3
 	github.com/imdario/mergo v0.3.11 // indirect
 	github.com/json-iterator/go v1.1.10
 	github.com/kr/text v0.2.0 // indirect
@@ -45,7 +45,7 @@ require (
 	github.com/spf13/viper v1.7.1
 	github.com/stoewer/go-strcase v1.2.0
 	github.com/stretchr/testify v1.7.0
-	go.opencensus.io v0.22.6
+	go.opencensus.io v0.23.0
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.16.0
 	golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
@@ -56,7 +56,7 @@ require (
 	golang.org/x/tools v0.0.0-20201121010211-780cb80bd7fb
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5 // indirect
-	google.golang.org/grpc v1.35.0
+	google.golang.org/grpc v1.36.0
 	google.golang.org/grpc/examples v0.0.0-20200707005602-4258d12073b4 // indirect
 	google.golang.org/protobuf v1.25.0
 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
diff --git a/go.sum b/go.sum
index b4c71b6e..648bc45f 100644
--- a/go.sum
+++ b/go.sum
@@ -331,8 +331,8 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe
 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/iancoleman/strcase v0.1.2 h1:gnomlvw9tnV3ITTAxzKSgTF+8kFWcU/f+TgttpXGz1U=
-github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
+github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw=
+github.com/iancoleman/strcase v0.1.3/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
@@ -541,8 +541,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.6 h1:BdkrbWrzDlV9dnbzoP7sfN+dHheJ4J9JOaYxcUDL+ok=
-go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@@ -854,8 +854,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
 google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
 google.golang.org/grpc/examples v0.0.0-20200707005602-4258d12073b4 h1:aO5Yh7ygpoceTmlzR0dl6xC4LKYmrkLcW0MBGx2uyBg=
 google.golang.org/grpc/examples v0.0.0-20200707005602-4258d12073b4/go.mod h1:5j1uub0jRGhRiSghIlrThmBUgcgLXOVJQ/l1getT4uo=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
diff --git a/pkg/dash/dash.go b/pkg/dash/dash.go
index 55169e14..4817043a 100644
--- a/pkg/dash/dash.go
+++ b/pkg/dash/dash.go
@@ -363,7 +363,6 @@ func (r *Runner) initAPI(ctx context.Context, logger log.Logger, opts ...RunnerO
 		}
 	}
 
-	fmt.Println(options.EnableMemStats)
 	if options.EnableMemStats {
 		if err := memStats(); err != nil {
 			logger.Infof("Enable MemStat")
diff --git a/pkg/plugin/client.go b/pkg/plugin/client.go
index 27fb5f76..12b36d26 100644
--- a/pkg/plugin/client.go
+++ b/pkg/plugin/client.go
@@ -94,7 +94,7 @@ type Metadata struct {
 type Service interface {
 	Register(ctx context.Context, dashboardAPIAddress string) (Metadata, error)
 	Print(ctx context.Context, object runtime.Object) (PrintResponse, error)
-	PrintTab(ctx context.Context, object runtime.Object) (TabResponse, error)
+	PrintTabs(ctx context.Context, object runtime.Object) ([]TabResponse, error)
 	ObjectStatus(ctx context.Context, object runtime.Object) (ObjectStatusResponse, error)
 	HandleAction(ctx context.Context, actionName string, payload action.Payload) error
 }
diff --git a/pkg/plugin/dashboard/dashboard.pb.go b/pkg/plugin/dashboard/dashboard.pb.go
index 7a0d0d5c..e25a3222 100644
--- a/pkg/plugin/dashboard/dashboard.pb.go
+++ b/pkg/plugin/dashboard/dashboard.pb.go
@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.23.0
-// 	protoc        v3.7.1
+// 	protoc-gen-go v1.25.0
+// 	protoc        v3.12.4
 // source: dashboard.proto
 
 package dashboard
@@ -598,8 +598,7 @@ type PrintTabResponse struct {
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
-	Name   string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
-	Layout []byte `protobuf:"bytes,2,opt,name=layout,proto3" json:"layout,omitempty"`
+	Tabs []*PrintTab `protobuf:"bytes,1,rep,name=tabs,proto3" json:"tabs,omitempty"`
 }
 
 func (x *PrintTabResponse) Reset() {
@@ -634,14 +633,62 @@ func (*PrintTabResponse) Descriptor() ([]byte, []int) {
 	return file_dashboard_proto_rawDescGZIP(), []int{11}
 }
 
-func (x *PrintTabResponse) GetName() string {
+func (x *PrintTabResponse) GetTabs() []*PrintTab {
+	if x != nil {
+		return x.Tabs
+	}
+	return nil
+}
+
+type PrintTab struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Name   string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Layout []byte `protobuf:"bytes,2,opt,name=layout,proto3" json:"layout,omitempty"`
+}
+
+func (x *PrintTab) Reset() {
+	*x = PrintTab{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_dashboard_proto_msgTypes[12]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PrintTab) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PrintTab) ProtoMessage() {}
+
+func (x *PrintTab) ProtoReflect() protoreflect.Message {
+	mi := &file_dashboard_proto_msgTypes[12]
+	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 PrintTab.ProtoReflect.Descriptor instead.
+func (*PrintTab) Descriptor() ([]byte, []int) {
+	return file_dashboard_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *PrintTab) GetName() string {
 	if x != nil {
 		return x.Name
 	}
 	return ""
 }
 
-func (x *PrintTabResponse) GetLayout() []byte {
+func (x *PrintTab) GetLayout() []byte {
 	if x != nil {
 		return x.Layout
 	}
@@ -659,7 +706,7 @@ type ObjectStatusResponse struct {
 func (x *ObjectStatusResponse) Reset() {
 	*x = ObjectStatusResponse{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_dashboard_proto_msgTypes[12]
+		mi := &file_dashboard_proto_msgTypes[13]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -672,7 +719,7 @@ func (x *ObjectStatusResponse) String() string {
 func (*ObjectStatusResponse) ProtoMessage() {}
 
 func (x *ObjectStatusResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_dashboard_proto_msgTypes[12]
+	mi := &file_dashboard_proto_msgTypes[13]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -685,7 +732,7 @@ func (x *ObjectStatusResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use ObjectStatusResponse.ProtoReflect.Descriptor instead.
 func (*ObjectStatusResponse) Descriptor() ([]byte, []int) {
-	return file_dashboard_proto_rawDescGZIP(), []int{12}
+	return file_dashboard_proto_rawDescGZIP(), []int{13}
 }
 
 func (x *ObjectStatusResponse) GetObjectStatus() []byte {
@@ -707,7 +754,7 @@ type WatchRequest struct {
 func (x *WatchRequest) Reset() {
 	*x = WatchRequest{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_dashboard_proto_msgTypes[13]
+		mi := &file_dashboard_proto_msgTypes[14]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -720,7 +767,7 @@ func (x *WatchRequest) String() string {
 func (*WatchRequest) ProtoMessage() {}
 
 func (x *WatchRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_dashboard_proto_msgTypes[13]
+	mi := &file_dashboard_proto_msgTypes[14]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -733,7 +780,7 @@ func (x *WatchRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use WatchRequest.ProtoReflect.Descriptor instead.
 func (*WatchRequest) Descriptor() ([]byte, []int) {
-	return file_dashboard_proto_rawDescGZIP(), []int{13}
+	return file_dashboard_proto_rawDescGZIP(), []int{14}
 }
 
 func (x *WatchRequest) GetWatchID() string {
@@ -765,7 +812,7 @@ type NavigationResponse_Navigation struct {
 func (x *NavigationResponse_Navigation) Reset() {
 	*x = NavigationResponse_Navigation{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_dashboard_proto_msgTypes[14]
+		mi := &file_dashboard_proto_msgTypes[15]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -778,7 +825,7 @@ func (x *NavigationResponse_Navigation) String() string {
 func (*NavigationResponse_Navigation) ProtoMessage() {}
 
 func (x *NavigationResponse_Navigation) ProtoReflect() protoreflect.Message {
-	mi := &file_dashboard_proto_msgTypes[14]
+	mi := &file_dashboard_proto_msgTypes[15]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -842,7 +889,7 @@ type RegisterResponse_GroupVersionKind struct {
 func (x *RegisterResponse_GroupVersionKind) Reset() {
 	*x = RegisterResponse_GroupVersionKind{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_dashboard_proto_msgTypes[15]
+		mi := &file_dashboard_proto_msgTypes[16]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -855,7 +902,7 @@ func (x *RegisterResponse_GroupVersionKind) String() string {
 func (*RegisterResponse_GroupVersionKind) ProtoMessage() {}
 
 func (x *RegisterResponse_GroupVersionKind) ProtoReflect() protoreflect.Message {
-	mi := &file_dashboard_proto_msgTypes[15]
+	mi := &file_dashboard_proto_msgTypes[16]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -909,7 +956,7 @@ type RegisterResponse_Capabilities struct {
 func (x *RegisterResponse_Capabilities) Reset() {
 	*x = RegisterResponse_Capabilities{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_dashboard_proto_msgTypes[16]
+		mi := &file_dashboard_proto_msgTypes[17]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -922,7 +969,7 @@ func (x *RegisterResponse_Capabilities) String() string {
 func (*RegisterResponse_Capabilities) ProtoMessage() {}
 
 func (x *RegisterResponse_Capabilities) ProtoReflect() protoreflect.Message {
-	mi := &file_dashboard_proto_msgTypes[16]
+	mi := &file_dashboard_proto_msgTypes[17]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -999,7 +1046,7 @@ type PrintResponse_SummaryItem struct {
 func (x *PrintResponse_SummaryItem) Reset() {
 	*x = PrintResponse_SummaryItem{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_dashboard_proto_msgTypes[17]
+		mi := &file_dashboard_proto_msgTypes[18]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1012,7 +1059,7 @@ func (x *PrintResponse_SummaryItem) String() string {
 func (*PrintResponse_SummaryItem) ProtoMessage() {}
 
 func (x *PrintResponse_SummaryItem) ProtoReflect() protoreflect.Message {
-	mi := &file_dashboard_proto_msgTypes[17]
+	mi := &file_dashboard_proto_msgTypes[18]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1158,61 +1205,65 @@ var file_dashboard_proto_rawDesc = []byte{
 	0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
 	0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x70,
 	0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x63, 0x6f, 0x6d,
-	0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x22, 0x3e, 0x0a, 0x10, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x54,
-	0x61, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
-	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16,
-	0x0a, 0x06, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06,
-	0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x22, 0x3a, 0x0a, 0x14, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74,
-	0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22,
-	0x0a, 0x0c, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74,
-	0x75, 0x73, 0x22, 0x40, 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65,
-	0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x61, 0x74, 0x63, 0x68, 0x49, 0x44, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x09, 0x52, 0x07, 0x77, 0x61, 0x74, 0x63, 0x68, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06,
-	0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x62,
-	0x6a, 0x65, 0x63, 0x74, 0x32, 0xa1, 0x05, 0x0a, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12,
-	0x40, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x73,
+	0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x22, 0x3b, 0x0a, 0x10, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x54,
+	0x61, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x04, 0x74, 0x61,
+	0x62, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62,
+	0x6f, 0x61, 0x72, 0x64, 0x2e, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x52, 0x04, 0x74,
+	0x61, 0x62, 0x73, 0x22, 0x36, 0x0a, 0x08, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x12,
+	0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
+	0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x0c, 0x52, 0x06, 0x6c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x22, 0x3a, 0x0a, 0x14, 0x4f,
+	0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61,
+	0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x6f, 0x62, 0x6a, 0x65, 0x63,
+	0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x40, 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x61, 0x74, 0x63, 0x68,
+	0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x77, 0x61, 0x74, 0x63, 0x68, 0x49,
+	0x44, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x0c, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x32, 0xa2, 0x05, 0x0a, 0x06, 0x50, 0x6c,
+	0x75, 0x67, 0x69, 0x6e, 0x12, 0x40, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12,
+	0x19, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6e, 0x74,
+	0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x73,
 	0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72,
-	0x64, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
-	0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f,
-	0x6e, 0x12, 0x1e, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x48, 0x61,
-	0x6e, 0x64, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x48, 0x61,
-	0x6e, 0x64, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x4e, 0x61, 0x76, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e,
-	0x12, 0x1c, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x4e, 0x61, 0x76,
-	0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d,
-	0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x4e, 0x61, 0x76, 0x69, 0x67,
-	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a,
-	0x08, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x73, 0x68,
-	0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72,
-	0x64, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
-	0x73, 0x65, 0x12, 0x3b, 0x0a, 0x05, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x18, 0x2e, 0x64, 0x61,
-	0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65,
-	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72,
-	0x64, 0x2e, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
-	0x49, 0x0a, 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
-	0x18, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65,
-	0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x73, 0x68,
-	0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74,
-	0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x08, 0x50, 0x72,
-	0x69, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61,
-	0x72, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x50, 0x72, 0x69,
-	0x6e, 0x74, 0x54, 0x61, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a,
-	0x08, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x64, 0x64, 0x12, 0x17, 0x2e, 0x64, 0x61, 0x73, 0x68,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65,
+	0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61,
+	0x72, 0x64, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61,
+	0x72, 0x64, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x4e, 0x61, 0x76, 0x69, 0x67,
+	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72,
+	0x64, 0x2e, 0x4e, 0x61, 0x76, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e,
+	0x4e, 0x61, 0x76, 0x69, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x1a,
+	0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73,
+	0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x73,
+	0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x05, 0x50, 0x72, 0x69, 0x6e, 0x74,
+	0x12, 0x18, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x4f, 0x62, 0x6a,
+	0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x64, 0x61, 0x73,
+	0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74,
+	0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64,
+	0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f,
+	0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63,
+	0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+	0x42, 0x0a, 0x09, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x73, 0x12, 0x18, 0x2e, 0x64,
+	0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61,
+	0x72, 0x64, 0x2e, 0x50, 0x72, 0x69, 0x6e, 0x74, 0x54, 0x61, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x08, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x64, 0x64, 0x12,
+	0x17, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x57, 0x61, 0x74, 0x63,
+	0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62,
+	0x6f, 0x61, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x0b, 0x57, 0x61,
+	0x74, 0x63, 0x68, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x64, 0x61, 0x73, 0x68,
 	0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65,
 	0x73, 0x74, 0x1a, 0x10, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x45,
-	0x6d, 0x70, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x0b, 0x57, 0x61, 0x74, 0x63, 0x68, 0x55, 0x70, 0x64,
-	0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e,
+	0x6d, 0x70, 0x74, 0x79, 0x12, 0x38, 0x0a, 0x0b, 0x57, 0x61, 0x74, 0x63, 0x68, 0x44, 0x65, 0x6c,
+	0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e,
 	0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x64,
-	0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x38,
-	0x0a, 0x0b, 0x57, 0x61, 0x74, 0x63, 0x68, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e,
-	0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52,
-	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x64, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61,
-	0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -1227,7 +1278,7 @@ func file_dashboard_proto_rawDescGZIP() []byte {
 	return file_dashboard_proto_rawDescData
 }
 
-var file_dashboard_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
+var file_dashboard_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
 var file_dashboard_proto_goTypes = []interface{}{
 	(*Empty)(nil),                             // 0: dashboard.Empty
 	(*ContentRequest)(nil),                    // 1: dashboard.ContentRequest
@@ -1241,49 +1292,51 @@ var file_dashboard_proto_goTypes = []interface{}{
 	(*ObjectRequest)(nil),                     // 9: dashboard.ObjectRequest
 	(*PrintResponse)(nil),                     // 10: dashboard.PrintResponse
 	(*PrintTabResponse)(nil),                  // 11: dashboard.PrintTabResponse
-	(*ObjectStatusResponse)(nil),              // 12: dashboard.ObjectStatusResponse
-	(*WatchRequest)(nil),                      // 13: dashboard.WatchRequest
-	(*NavigationResponse_Navigation)(nil),     // 14: dashboard.NavigationResponse.Navigation
-	(*RegisterResponse_GroupVersionKind)(nil), // 15: dashboard.RegisterResponse.GroupVersionKind
-	(*RegisterResponse_Capabilities)(nil),     // 16: dashboard.RegisterResponse.Capabilities
-	(*PrintResponse_SummaryItem)(nil),         // 17: dashboard.PrintResponse.SummaryItem
+	(*PrintTab)(nil),                          // 12: dashboard.PrintTab
+	(*ObjectStatusResponse)(nil),              // 13: dashboard.ObjectStatusResponse
+	(*WatchRequest)(nil),                      // 14: dashboard.WatchRequest
+	(*NavigationResponse_Navigation)(nil),     // 15: dashboard.NavigationResponse.Navigation
+	(*RegisterResponse_GroupVersionKind)(nil), // 16: dashboard.RegisterResponse.GroupVersionKind
+	(*RegisterResponse_Capabilities)(nil),     // 17: dashboard.RegisterResponse.Capabilities
+	(*PrintResponse_SummaryItem)(nil),         // 18: dashboard.PrintResponse.SummaryItem
 }
 var file_dashboard_proto_depIdxs = []int32{
-	14, // 0: dashboard.NavigationResponse.navigation:type_name -> dashboard.NavigationResponse.Navigation
-	16, // 1: dashboard.RegisterResponse.capabilities:type_name -> dashboard.RegisterResponse.Capabilities
-	17, // 2: dashboard.PrintResponse.config:type_name -> dashboard.PrintResponse.SummaryItem
-	17, // 3: dashboard.PrintResponse.status:type_name -> dashboard.PrintResponse.SummaryItem
-	14, // 4: dashboard.NavigationResponse.Navigation.children:type_name -> dashboard.NavigationResponse.Navigation
-	15, // 5: dashboard.RegisterResponse.Capabilities.supportsPrinterConfig:type_name -> dashboard.RegisterResponse.GroupVersionKind
-	15, // 6: dashboard.RegisterResponse.Capabilities.supportsPrinterStatus:type_name -> dashboard.RegisterResponse.GroupVersionKind
-	15, // 7: dashboard.RegisterResponse.Capabilities.supportsPrinterItems:type_name -> dashboard.RegisterResponse.GroupVersionKind
-	15, // 8: dashboard.RegisterResponse.Capabilities.supportsObjectStatus:type_name -> dashboard.RegisterResponse.GroupVersionKind
-	15, // 9: dashboard.RegisterResponse.Capabilities.supportsTab:type_name -> dashboard.RegisterResponse.GroupVersionKind
-	1,  // 10: dashboard.Plugin.Content:input_type -> dashboard.ContentRequest
-	3,  // 11: dashboard.Plugin.HandleAction:input_type -> dashboard.HandleActionRequest
-	5,  // 12: dashboard.Plugin.Navigation:input_type -> dashboard.NavigationRequest
-	7,  // 13: dashboard.Plugin.Register:input_type -> dashboard.RegisterRequest
-	9,  // 14: dashboard.Plugin.Print:input_type -> dashboard.ObjectRequest
-	9,  // 15: dashboard.Plugin.ObjectStatus:input_type -> dashboard.ObjectRequest
-	9,  // 16: dashboard.Plugin.PrintTab:input_type -> dashboard.ObjectRequest
-	13, // 17: dashboard.Plugin.WatchAdd:input_type -> dashboard.WatchRequest
-	13, // 18: dashboard.Plugin.WatchUpdate:input_type -> dashboard.WatchRequest
-	13, // 19: dashboard.Plugin.WatchDelete:input_type -> dashboard.WatchRequest
-	2,  // 20: dashboard.Plugin.Content:output_type -> dashboard.ContentResponse
-	4,  // 21: dashboard.Plugin.HandleAction:output_type -> dashboard.HandleActionResponse
-	6,  // 22: dashboard.Plugin.Navigation:output_type -> dashboard.NavigationResponse
-	8,  // 23: dashboard.Plugin.Register:output_type -> dashboard.RegisterResponse
-	10, // 24: dashboard.Plugin.Print:output_type -> dashboard.PrintResponse
-	12, // 25: dashboard.Plugin.ObjectStatus:output_type -> dashboard.ObjectStatusResponse
-	11, // 26: dashboard.Plugin.PrintTab:output_type -> dashboard.PrintTabResponse
-	0,  // 27: dashboard.Plugin.WatchAdd:output_type -> dashboard.Empty
-	0,  // 28: dashboard.Plugin.WatchUpdate:output_type -> dashboard.Empty
-	0,  // 29: dashboard.Plugin.WatchDelete:output_type -> dashboard.Empty
-	20, // [20:30] is the sub-list for method output_type
-	10, // [10:20] is the sub-list for method input_type
-	10, // [10:10] is the sub-list for extension type_name
-	10, // [10:10] is the sub-list for extension extendee
-	0,  // [0:10] is the sub-list for field type_name
+	15, // 0: dashboard.NavigationResponse.navigation:type_name -> dashboard.NavigationResponse.Navigation
+	17, // 1: dashboard.RegisterResponse.capabilities:type_name -> dashboard.RegisterResponse.Capabilities
+	18, // 2: dashboard.PrintResponse.config:type_name -> dashboard.PrintResponse.SummaryItem
+	18, // 3: dashboard.PrintResponse.status:type_name -> dashboard.PrintResponse.SummaryItem
+	12, // 4: dashboard.PrintTabResponse.tabs:type_name -> dashboard.PrintTab
+	15, // 5: dashboard.NavigationResponse.Navigation.children:type_name -> dashboard.NavigationResponse.Navigation
+	16, // 6: dashboard.RegisterResponse.Capabilities.supportsPrinterConfig:type_name -> dashboard.RegisterResponse.GroupVersionKind
+	16, // 7: dashboard.RegisterResponse.Capabilities.supportsPrinterStatus:type_name -> dashboard.RegisterResponse.GroupVersionKind
+	16, // 8: dashboard.RegisterResponse.Capabilities.supportsPrinterItems:type_name -> dashboard.RegisterResponse.GroupVersionKind
+	16, // 9: dashboard.RegisterResponse.Capabilities.supportsObjectStatus:type_name -> dashboard.RegisterResponse.GroupVersionKind
+	16, // 10: dashboard.RegisterResponse.Capabilities.supportsTab:type_name -> dashboard.RegisterResponse.GroupVersionKind
+	1,  // 11: dashboard.Plugin.Content:input_type -> dashboard.ContentRequest
+	3,  // 12: dashboard.Plugin.HandleAction:input_type -> dashboard.HandleActionRequest
+	5,  // 13: dashboard.Plugin.Navigation:input_type -> dashboard.NavigationRequest
+	7,  // 14: dashboard.Plugin.Register:input_type -> dashboard.RegisterRequest
+	9,  // 15: dashboard.Plugin.Print:input_type -> dashboard.ObjectRequest
+	9,  // 16: dashboard.Plugin.ObjectStatus:input_type -> dashboard.ObjectRequest
+	9,  // 17: dashboard.Plugin.PrintTabs:input_type -> dashboard.ObjectRequest
+	14, // 18: dashboard.Plugin.WatchAdd:input_type -> dashboard.WatchRequest
+	14, // 19: dashboard.Plugin.WatchUpdate:input_type -> dashboard.WatchRequest
+	14, // 20: dashboard.Plugin.WatchDelete:input_type -> dashboard.WatchRequest
+	2,  // 21: dashboard.Plugin.Content:output_type -> dashboard.ContentResponse
+	4,  // 22: dashboard.Plugin.HandleAction:output_type -> dashboard.HandleActionResponse
+	6,  // 23: dashboard.Plugin.Navigation:output_type -> dashboard.NavigationResponse
+	8,  // 24: dashboard.Plugin.Register:output_type -> dashboard.RegisterResponse
+	10, // 25: dashboard.Plugin.Print:output_type -> dashboard.PrintResponse
+	13, // 26: dashboard.Plugin.ObjectStatus:output_type -> dashboard.ObjectStatusResponse
+	11, // 27: dashboard.Plugin.PrintTabs:output_type -> dashboard.PrintTabResponse
+	0,  // 28: dashboard.Plugin.WatchAdd:output_type -> dashboard.Empty
+	0,  // 29: dashboard.Plugin.WatchUpdate:output_type -> dashboard.Empty
+	0,  // 30: dashboard.Plugin.WatchDelete:output_type -> dashboard.Empty
+	21, // [21:31] is the sub-list for method output_type
+	11, // [11:21] is the sub-list for method input_type
+	11, // [11:11] is the sub-list for extension type_name
+	11, // [11:11] is the sub-list for extension extendee
+	0,  // [0:11] is the sub-list for field type_name
 }
 
 func init() { file_dashboard_proto_init() }
@@ -1437,7 +1490,7 @@ func file_dashboard_proto_init() {
 			}
 		}
 		file_dashboard_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ObjectStatusResponse); i {
+			switch v := v.(*PrintTab); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -1449,7 +1502,7 @@ func file_dashboard_proto_init() {
 			}
 		}
 		file_dashboard_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*WatchRequest); i {
+			switch v := v.(*ObjectStatusResponse); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -1461,7 +1514,7 @@ func file_dashboard_proto_init() {
 			}
 		}
 		file_dashboard_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*NavigationResponse_Navigation); i {
+			switch v := v.(*WatchRequest); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -1473,7 +1526,7 @@ func file_dashboard_proto_init() {
 			}
 		}
 		file_dashboard_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*RegisterResponse_GroupVersionKind); i {
+			switch v := v.(*NavigationResponse_Navigation); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -1485,7 +1538,7 @@ func file_dashboard_proto_init() {
 			}
 		}
 		file_dashboard_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*RegisterResponse_Capabilities); i {
+			switch v := v.(*RegisterResponse_GroupVersionKind); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -1497,6 +1550,18 @@ func file_dashboard_proto_init() {
 			}
 		}
 		file_dashboard_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*RegisterResponse_Capabilities); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_dashboard_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*PrintResponse_SummaryItem); i {
 			case 0:
 				return &v.state
@@ -1515,7 +1580,7 @@ func file_dashboard_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_dashboard_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   18,
+			NumMessages:   19,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
@@ -1547,7 +1612,7 @@ type PluginClient interface {
 	Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error)
 	Print(ctx context.Context, in *ObjectRequest, opts ...grpc.CallOption) (*PrintResponse, error)
 	ObjectStatus(ctx context.Context, in *ObjectRequest, opts ...grpc.CallOption) (*ObjectStatusResponse, error)
-	PrintTab(ctx context.Context, in *ObjectRequest, opts ...grpc.CallOption) (*PrintTabResponse, error)
+	PrintTabs(ctx context.Context, in *ObjectRequest, opts ...grpc.CallOption) (*PrintTabResponse, error)
 	WatchAdd(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (*Empty, error)
 	WatchUpdate(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (*Empty, error)
 	WatchDelete(ctx context.Context, in *WatchRequest, opts ...grpc.CallOption) (*Empty, error)
@@ -1615,9 +1680,9 @@ func (c *pluginClient) ObjectStatus(ctx context.Context, in *ObjectRequest, opts
 	return out, nil
 }
 
-func (c *pluginClient) PrintTab(ctx context.Context, in *ObjectRequest, opts ...grpc.CallOption) (*PrintTabResponse, error) {
+func (c *pluginClient) PrintTabs(ctx context.Context, in *ObjectRequest, opts ...grpc.CallOption) (*PrintTabResponse, error) {
 	out := new(PrintTabResponse)
-	err := c.cc.Invoke(ctx, "/dashboard.Plugin/PrintTab", in, out, opts...)
+	err := c.cc.Invoke(ctx, "/dashboard.Plugin/PrintTabs", in, out, opts...)
 	if err != nil {
 		return nil, err
 	}
@@ -1659,7 +1724,7 @@ type PluginServer interface {
 	Register(context.Context, *RegisterRequest) (*RegisterResponse, error)
 	Print(context.Context, *ObjectRequest) (*PrintResponse, error)
 	ObjectStatus(context.Context, *ObjectRequest) (*ObjectStatusResponse, error)
-	PrintTab(context.Context, *ObjectRequest) (*PrintTabResponse, error)
+	PrintTabs(context.Context, *ObjectRequest) (*PrintTabResponse, error)
 	WatchAdd(context.Context, *WatchRequest) (*Empty, error)
 	WatchUpdate(context.Context, *WatchRequest) (*Empty, error)
 	WatchDelete(context.Context, *WatchRequest) (*Empty, error)
@@ -1687,8 +1752,8 @@ func (*UnimplementedPluginServer) Print(context.Context, *ObjectRequest) (*Print
 func (*UnimplementedPluginServer) ObjectStatus(context.Context, *ObjectRequest) (*ObjectStatusResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method ObjectStatus not implemented")
 }
-func (*UnimplementedPluginServer) PrintTab(context.Context, *ObjectRequest) (*PrintTabResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method PrintTab not implemented")
+func (*UnimplementedPluginServer) PrintTabs(context.Context, *ObjectRequest) (*PrintTabResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method PrintTabs not implemented")
 }
 func (*UnimplementedPluginServer) WatchAdd(context.Context, *WatchRequest) (*Empty, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method WatchAdd not implemented")
@@ -1812,20 +1877,20 @@ func _Plugin_ObjectStatus_Handler(srv interface{}, ctx context.Context, dec func
 	return interceptor(ctx, in, info, handler)
 }
 
-func _Plugin_PrintTab_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+func _Plugin_PrintTabs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(ObjectRequest)
 	if err := dec(in); err != nil {
 		return nil, err
 	}
 	if interceptor == nil {
-		return srv.(PluginServer).PrintTab(ctx, in)
+		return srv.(PluginServer).PrintTabs(ctx, in)
 	}
 	info := &grpc.UnaryServerInfo{
 		Server:     srv,
-		FullMethod: "/dashboard.Plugin/PrintTab",
+		FullMethod: "/dashboard.Plugin/PrintTabs",
 	}
 	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(PluginServer).PrintTab(ctx, req.(*ObjectRequest))
+		return srv.(PluginServer).PrintTabs(ctx, req.(*ObjectRequest))
 	}
 	return interceptor(ctx, in, info, handler)
 }
@@ -1913,8 +1978,8 @@ var _Plugin_serviceDesc = grpc.ServiceDesc{
 			Handler:    _Plugin_ObjectStatus_Handler,
 		},
 		{
-			MethodName: "PrintTab",
-			Handler:    _Plugin_PrintTab_Handler,
+			MethodName: "PrintTabs",
+			Handler:    _Plugin_PrintTabs_Handler,
 		},
 		{
 			MethodName: "WatchAdd",
diff --git a/pkg/plugin/dashboard/dashboard.proto b/pkg/plugin/dashboard/dashboard.proto
index d0dbdc2a..1e1e0fe4 100644
--- a/pkg/plugin/dashboard/dashboard.proto
+++ b/pkg/plugin/dashboard/dashboard.proto
@@ -80,6 +80,10 @@ message PrintResponse {
 }
 
 message PrintTabResponse {
+    repeated PrintTab tabs = 1;
+}
+
+message PrintTab {
     string name = 1;
     bytes layout = 2;
 }
@@ -100,7 +104,7 @@ service Plugin {
     rpc Register(RegisterRequest) returns (RegisterResponse);
     rpc Print(ObjectRequest) returns (PrintResponse);
     rpc ObjectStatus(ObjectRequest) returns (ObjectStatusResponse);
-    rpc PrintTab(ObjectRequest) returns (PrintTabResponse);
+    rpc PrintTabs(ObjectRequest) returns (PrintTabResponse);
     rpc WatchAdd(WatchRequest) returns (Empty);
     rpc WatchUpdate(WatchRequest) returns (Empty);
     rpc WatchDelete(WatchRequest) returns (Empty);
diff --git a/pkg/plugin/fake/fakes.go b/pkg/plugin/fake/fakes.go
index 04258c7d..4164eb9b 100644
--- a/pkg/plugin/fake/fakes.go
+++ b/pkg/plugin/fake/fakes.go
@@ -72,11 +72,11 @@ func (mr *MockRunnersMockRecorder) Print(arg0 interface{}) *gomock.Call {
 }
 
 // Tab mocks base method
-func (m *MockRunners) Tab(arg0 plugin.ManagerStore) (plugin.DefaultRunner, chan component.Tab) {
+func (m *MockRunners) Tab(arg0 plugin.ManagerStore) (plugin.DefaultRunner, chan []component.Tab) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Tab", arg0)
 	ret0, _ := ret[0].(plugin.DefaultRunner)
-	ret1, _ := ret[1].(chan component.Tab)
+	ret1, _ := ret[1].(chan []component.Tab)
 	return ret0, ret1
 }
 
@@ -385,19 +385,19 @@ func (mr *MockModuleServiceMockRecorder) Print(arg0, arg1 interface{}) *gomock.C
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Print", reflect.TypeOf((*MockModuleService)(nil).Print), arg0, arg1)
 }
 
-// PrintTab mocks base method
-func (m *MockModuleService) PrintTab(arg0 context.Context, arg1 runtime.Object) (plugin.TabResponse, error) {
+// PrintTabs mocks base method
+func (m *MockModuleService) PrintTabs(arg0 context.Context, arg1 runtime.Object) ([]plugin.TabResponse, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "PrintTab", arg0, arg1)
-	ret0, _ := ret[0].(plugin.TabResponse)
+	ret := m.ctrl.Call(m, "PrintTabs", arg0, arg1)
+	ret0, _ := ret[0].([]plugin.TabResponse)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// PrintTab indicates an expected call of PrintTab
-func (mr *MockModuleServiceMockRecorder) PrintTab(arg0, arg1 interface{}) *gomock.Call {
+// PrintTabs indicates an expected call of PrintTabs
+func (mr *MockModuleServiceMockRecorder) PrintTabs(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTab", reflect.TypeOf((*MockModuleService)(nil).PrintTab), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTabs", reflect.TypeOf((*MockModuleService)(nil).PrintTabs), arg0, arg1)
 }
 
 // Register mocks base method
@@ -482,19 +482,19 @@ func (mr *MockServiceMockRecorder) Print(arg0, arg1 interface{}) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Print", reflect.TypeOf((*MockService)(nil).Print), arg0, arg1)
 }
 
-// PrintTab mocks base method
-func (m *MockService) PrintTab(arg0 context.Context, arg1 runtime.Object) (plugin.TabResponse, error) {
+// PrintTabs mocks base method
+func (m *MockService) PrintTabs(arg0 context.Context, arg1 runtime.Object) ([]plugin.TabResponse, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "PrintTab", arg0, arg1)
-	ret0, _ := ret[0].(plugin.TabResponse)
+	ret := m.ctrl.Call(m, "PrintTabs", arg0, arg1)
+	ret0, _ := ret[0].([]plugin.TabResponse)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// PrintTab indicates an expected call of PrintTab
-func (mr *MockServiceMockRecorder) PrintTab(arg0, arg1 interface{}) *gomock.Call {
+// PrintTabs indicates an expected call of PrintTabs
+func (mr *MockServiceMockRecorder) PrintTabs(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTab", reflect.TypeOf((*MockService)(nil).PrintTab), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTabs", reflect.TypeOf((*MockService)(nil).PrintTabs), arg0, arg1)
 }
 
 // Register mocks base method
diff --git a/pkg/plugin/fake/mock_plugin_client.go b/pkg/plugin/fake/mock_plugin_client.go
index 1288f9f8..a1a31671 100644
--- a/pkg/plugin/fake/mock_plugin_client.go
+++ b/pkg/plugin/fake/mock_plugin_client.go
@@ -157,24 +157,24 @@ func (mr *MockPluginClientMockRecorder) ObjectStatus(ctx, in interface{}, opts .
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectStatus", reflect.TypeOf((*MockPluginClient)(nil).ObjectStatus), varargs...)
 }
 
-// PrintTab mocks base method
-func (m *MockPluginClient) PrintTab(ctx context.Context, in *dashboard.ObjectRequest, opts ...grpc.CallOption) (*dashboard.PrintTabResponse, error) {
+// PrintTabs mocks base method
+func (m *MockPluginClient) PrintTabs(ctx context.Context, in *dashboard.ObjectRequest, opts ...grpc.CallOption) (*dashboard.PrintTabResponse, error) {
 	m.ctrl.T.Helper()
 	varargs := []interface{}{ctx, in}
 	for _, a := range opts {
 		varargs = append(varargs, a)
 	}
-	ret := m.ctrl.Call(m, "PrintTab", varargs...)
+	ret := m.ctrl.Call(m, "PrintTabs", varargs...)
 	ret0, _ := ret[0].(*dashboard.PrintTabResponse)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// PrintTab indicates an expected call of PrintTab
-func (mr *MockPluginClientMockRecorder) PrintTab(ctx, in interface{}, opts ...interface{}) *gomock.Call {
+// PrintTabs indicates an expected call of PrintTabs
+func (mr *MockPluginClientMockRecorder) PrintTabs(ctx, in interface{}, opts ...interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	varargs := append([]interface{}{ctx, in}, opts...)
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTab", reflect.TypeOf((*MockPluginClient)(nil).PrintTab), varargs...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTabs", reflect.TypeOf((*MockPluginClient)(nil).PrintTabs), varargs...)
 }
 
 // WatchAdd mocks base method
@@ -350,19 +350,19 @@ func (mr *MockPluginServerMockRecorder) ObjectStatus(arg0, arg1 interface{}) *go
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectStatus", reflect.TypeOf((*MockPluginServer)(nil).ObjectStatus), arg0, arg1)
 }
 
-// PrintTab mocks base method
-func (m *MockPluginServer) PrintTab(arg0 context.Context, arg1 *dashboard.ObjectRequest) (*dashboard.PrintTabResponse, error) {
+// PrintTabs mocks base method
+func (m *MockPluginServer) PrintTabs(arg0 context.Context, arg1 *dashboard.ObjectRequest) (*dashboard.PrintTabResponse, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "PrintTab", arg0, arg1)
+	ret := m.ctrl.Call(m, "PrintTabs", arg0, arg1)
 	ret0, _ := ret[0].(*dashboard.PrintTabResponse)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// PrintTab indicates an expected call of PrintTab
-func (mr *MockPluginServerMockRecorder) PrintTab(arg0, arg1 interface{}) *gomock.Call {
+// PrintTabs indicates an expected call of PrintTabs
+func (mr *MockPluginServerMockRecorder) PrintTabs(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTab", reflect.TypeOf((*MockPluginServer)(nil).PrintTab), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrintTabs", reflect.TypeOf((*MockPluginServer)(nil).PrintTabs), arg0, arg1)
 }
 
 // WatchAdd mocks base method
diff --git a/pkg/plugin/grpc.go b/pkg/plugin/grpc.go
index 1fc898e1..963ee6d2 100644
--- a/pkg/plugin/grpc.go
+++ b/pkg/plugin/grpc.go
@@ -271,10 +271,9 @@ func createObjectRequest(object runtime.Object, clientID string) (*dashboard.Obj
 	return or, err
 }
 
-// PrintTab creates a tab for an object.
-func (c *GRPCClient) PrintTab(ctx context.Context, object runtime.Object) (TabResponse, error) {
-	var tab component.Tab
-
+// PrintTabs creates one or more tabs for an object.
+func (c *GRPCClient) PrintTabs(ctx context.Context, object runtime.Object) ([]TabResponse, error) {
+	var responses []TabResponse
 	clientID := ocontext.WebsocketClientIDFrom(ctx)
 
 	err := c.run(func() error {
@@ -283,37 +282,40 @@ func (c *GRPCClient) PrintTab(ctx context.Context, object runtime.Object) (TabRe
 			return err
 		}
 
-		resp, err := c.client.PrintTab(ctx, in, grpc.WaitForReady(true))
+		resp, err := c.client.PrintTabs(ctx, in, grpc.WaitForReady(true))
 		if err != nil {
 			return errors.Wrap(err, "grpc client print tab")
 		}
 
-		var to component.TypedObject
-		if err := json.Unmarshal(resp.Layout, &to); err != nil {
-			return err
-		}
-
-		c, err := to.ToComponent()
-		if err != nil {
-			return err
-		}
+		for _, t := range resp.Tabs {
+			var tab component.Tab
+			var to component.TypedObject
+			if err := json.Unmarshal(t.Layout, &to); err != nil {
+				return err
+			}
 
-		layout, ok := c.(*component.FlexLayout)
-		if !ok {
-			return errors.Errorf("expected to be flex layout was: %T", c)
-		}
+			c, err := to.ToComponent()
+			if err != nil {
+				return err
+			}
 
-		tab.Name = resp.Name
-		tab.Contents = *layout
+			layout, ok := c.(*component.FlexLayout)
+			if !ok {
+				return errors.Errorf("expected to be flex layout was: %T", c)
+			}
 
+			tab.Name = t.Name
+			tab.Contents = *layout
+			responses = append(responses, TabResponse{&tab})
+		}
 		return nil
 	})
 
 	if err != nil {
-		return TabResponse{}, err
+		return []TabResponse{}, err
 	}
 
-	return TabResponse{Tab: &tab}, nil
+	return responses, nil
 }
 
 // GRPCServer is the grpc server the dashboard will use to communicate with the
@@ -474,34 +476,40 @@ func decodeObjectRequest(req *dashboard.ObjectRequest) (*unstructured.Unstructur
 	return u, nil
 }
 
-// PrintTab prints a tab for an object.
-func (s *GRPCServer) PrintTab(ctx context.Context, objectRequest *dashboard.ObjectRequest) (*dashboard.PrintTabResponse, error) {
+// PrintTabs prints one or more tabs for an object.
+func (s *GRPCServer) PrintTabs(ctx context.Context, objectRequest *dashboard.ObjectRequest) (*dashboard.PrintTabResponse, error) {
 	u, err := decodeObjectRequest(objectRequest)
 	if err != nil {
 		return nil, err
 	}
 
 	ctx = ocontext.WithWebsocketClientID(ctx, objectRequest.ClientID)
-	tabResponse, err := s.Impl.PrintTab(ctx, u)
+	tabResponses, err := s.Impl.PrintTabs(ctx, u)
 	if err != nil {
 		return nil, errors.Wrap(err, "grpc server print tab")
 	}
 
-	if tabResponse.Tab == nil {
-		return nil, errors.New("tab is nil")
+	out := dashboard.PrintTabResponse{
+		Tabs: []*dashboard.PrintTab{},
 	}
+	for _, tabResponse := range tabResponses {
+		if tabResponse.Tab == nil {
+			return nil, errors.New("tab is nil")
+		}
 
-	layoutBytes, err := json.Marshal(tabResponse.Tab.Contents)
-	if err != nil {
-		return nil, err
-	}
+		layoutBytes, err := json.Marshal(tabResponse.Tab.Contents)
+		if err != nil {
+			return nil, err
+		}
 
-	out := &dashboard.PrintTabResponse{
-		Name:   tabResponse.Tab.Name,
-		Layout: layoutBytes,
+		tab := &dashboard.PrintTab{
+			Name:   tabResponse.Tab.Name,
+			Layout: layoutBytes,
+		}
+		out.Tabs = append(out.Tabs, tab)
 	}
 
-	return out, nil
+	return &out, nil
 }
 
 // WatchAdd is called when a watched GVK has a new object added.
diff --git a/pkg/plugin/javascript.go b/pkg/plugin/javascript.go
index a928c516..7c0f7a7d 100644
--- a/pkg/plugin/javascript.go
+++ b/pkg/plugin/javascript.go
@@ -75,7 +75,7 @@ type JSPlugin interface {
 	Navigation(ctx context.Context) (navigation.Navigation, error)
 	Register(ctx context.Context, dashboardAPIAddress string) (Metadata, error)
 	Print(ctx context.Context, object runtime.Object) (PrintResponse, error)
-	PrintTab(ctx context.Context, object runtime.Object) (TabResponse, error)
+	PrintTabs(ctx context.Context, object runtime.Object) ([]TabResponse, error)
 	ObjectStatus(ctx context.Context, object runtime.Object) (ObjectStatusResponse, error)
 	HandleAction(ctx context.Context, actionName string, payload action.Payload) error
 	Content(ctx context.Context, contentPath string) (component.ContentResponse, error)
@@ -361,24 +361,24 @@ func (t *jsPlugin) Register(_ context.Context, _ string) (Metadata, error) {
 	return Metadata{}, fmt.Errorf("not implemented")
 }
 
-// PrintTab returns the tab response from a JavaScript plugins tab handler.
-func (t *jsPlugin) PrintTab(ctx context.Context, object runtime.Object) (TabResponse, error) {
+// PrintTabs returns the tab(s) response from a JavaScript plugins tab handler.
+func (t *jsPlugin) PrintTabs(ctx context.Context, object runtime.Object) ([]TabResponse, error) {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
 	tabResponse, err := t.objectRequestCall(ctx, "tabHandler", object)
 	if err != nil {
-		return TabResponse{}, err
+		return []TabResponse{}, err
 	}
 
 	tab := tabResponse.Get("tab")
 	if tab == goja.Undefined() {
-		return TabResponse{}, fmt.Errorf("tab property not found")
+		return []TabResponse{}, fmt.Errorf("tab property not found")
 	}
 
 	contents, ok := tab.Export().(map[string]interface{})
 	if !ok {
-		return TabResponse{}, fmt.Errorf("unable to export tab contents")
+		return []TabResponse{}, fmt.Errorf("unable to export tab contents")
 	}
 
 	cTab := &component.Tab{}
@@ -390,14 +390,16 @@ func (t *jsPlugin) PrintTab(ctx context.Context, object runtime.Object) (TabResp
 	if contents, ok := contents["contents"]; ok {
 		realComponent, err := javascript.ConvertToComponent("tab contents", contents)
 		if err != nil {
-			return TabResponse{}, fmt.Errorf("unable to extract component: %w", err)
+			return []TabResponse{}, fmt.Errorf("unable to extract component: %w", err)
 		}
 		realFlexLayout := *realComponent.(*component.FlexLayout)
 		cTab.Contents.Config = realFlexLayout.Config
 	}
 
-	return TabResponse{
-		Tab: cTab,
+	return []TabResponse{
+		{
+			Tab: cTab,
+		},
 	}, nil
 }
 
diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go
index 213aa3bc..e44df839 100644
--- a/pkg/plugin/manager.go
+++ b/pkg/plugin/manager.go
@@ -736,8 +736,8 @@ func (m *Manager) Tabs(ctx context.Context, object runtime.Object) ([]component.
 	var tabs []component.Tab
 
 	go func() {
-		for tab := range ch {
-			tabs = append(tabs, tab)
+		for t := range ch {
+			tabs = append(tabs, t...)
 		}
 
 		done <- true
diff --git a/pkg/plugin/runner.go b/pkg/plugin/runner.go
index ff0d156a..2429f62f 100644
--- a/pkg/plugin/runner.go
+++ b/pkg/plugin/runner.go
@@ -24,7 +24,7 @@ type Runners interface {
 	Print(ManagerStore) (DefaultRunner, chan PrintResponse)
 	// Tab returns a runner for tabs. The caller should close
 	// the channel when they are done with it.
-	Tab(ManagerStore) (DefaultRunner, chan component.Tab)
+	Tab(ManagerStore) (DefaultRunner, chan []component.Tab)
 	// ObjectStatus returns a runner for object status. The caller should
 	// close the channel when they are done with it.
 	ObjectStatus(ManagerStore) (DefaultRunner, chan ObjectStatusResponse)
@@ -43,8 +43,8 @@ func (dr *defaultRunners) Print(store ManagerStore) (DefaultRunner, chan PrintRe
 	return PrintRunner(store, ch), ch
 }
 
-func (dr *defaultRunners) Tab(store ManagerStore) (DefaultRunner, chan component.Tab) {
-	ch := make(chan component.Tab)
+func (dr *defaultRunners) Tab(store ManagerStore) (DefaultRunner, chan []component.Tab) {
+	ch := make(chan []component.Tab)
 	return TabRunner(store, ch), ch
 }
 
@@ -161,7 +161,7 @@ func printObject(ctx context.Context, store ManagerStore, pluginName string, obj
 }
 
 // TabRunner is a runner for tabs.
-func TabRunner(store ManagerStore, ch chan<- component.Tab) DefaultRunner {
+func TabRunner(store ManagerStore, ch chan<- []component.Tab) DefaultRunner {
 	runner := DefaultRunner{
 		RunFunc: func(ctx context.Context, name string, gvk schema.GroupVersionKind, object runtime.Object) error {
 			if IsJavaScriptPlugin(name) {
@@ -174,12 +174,16 @@ func TabRunner(store ManagerStore, ch chan<- component.Tab) DefaultRunner {
 					return nil
 				}
 
-				resp, err := jsPlugin.PrintTab(ctx, object)
+				responses, err := jsPlugin.PrintTabs(ctx, object)
 				if err != nil {
 					return fmt.Errorf("printing tabResponse for plugin: %q: %w", name, err)
 				}
 
-				ch <- *resp.Tab
+				var jsTabs []component.Tab
+				for _, resp := range responses {
+					jsTabs = append(jsTabs, *resp.Tab)
+				}
+				ch <- jsTabs
 				return nil
 			}
 
@@ -201,12 +205,17 @@ func TabRunner(store ManagerStore, ch chan<- component.Tab) DefaultRunner {
 				return err
 			}
 
-			tabResponse, err := service.PrintTab(ctx, object)
+			tabResponses, err := service.PrintTabs(ctx, object)
 			if err != nil {
 				return fmt.Errorf("printing tabResponse for plugin %q: %w", name, err)
 			}
 
-			ch <- *tabResponse.Tab
+			var tabs []component.Tab
+			for _, tabResponse := range tabResponses {
+				tabs = append(tabs, *tabResponse.Tab)
+			}
+
+			ch <- tabs
 
 			return nil
 		},
diff --git a/pkg/plugin/service/handler.go b/pkg/plugin/service/handler.go
index 14dc0246..666bc2be 100644
--- a/pkg/plugin/service/handler.go
+++ b/pkg/plugin/service/handler.go
@@ -78,10 +78,10 @@ func (p *Handler) Print(ctx context.Context, object runtime.Object) (plugin.Prin
 	return p.HandlerFuncs.Print(request)
 }
 
-// PrintTab prints a tab for an object.
-func (p *Handler) PrintTab(ctx context.Context, object runtime.Object) (plugin.TabResponse, error) {
-	if p.HandlerFuncs.PrintTab == nil {
-		return plugin.TabResponse{}, nil
+// PrintTabs prints one or more tabs for an object.
+func (p *Handler) PrintTabs(ctx context.Context, object runtime.Object) ([]plugin.TabResponse, error) {
+	if p.HandlerFuncs.PrintTabs == nil {
+		return []plugin.TabResponse{}, nil
 	}
 
 	request := &PrintRequest{
@@ -91,7 +91,15 @@ func (p *Handler) PrintTab(ctx context.Context, object runtime.Object) (plugin.T
 		ClientID:        ocontext.WebsocketClientIDFrom(ctx),
 	}
 
-	return p.HandlerFuncs.PrintTab(request)
+	var tabResponses []plugin.TabResponse
+	for _, handlerFunc := range p.HandlerFuncs.PrintTabs {
+		resp, err := handlerFunc(request)
+		if err != nil {
+			return []plugin.TabResponse{}, err
+		}
+		tabResponses = append(tabResponses, resp)
+	}
+	return tabResponses, nil
 }
 
 // ObjectStatus creates status for an object.
diff --git a/pkg/plugin/service/service.go b/pkg/plugin/service/service.go
index 6d643fcc..d7f20fc9 100644
--- a/pkg/plugin/service/service.go
+++ b/pkg/plugin/service/service.go
@@ -33,9 +33,9 @@ func WithPrinter(fn HandlerPrinterFunc) PluginOption {
 }
 
 // WithTabPrinter configures the plugin to have a tab printer.
-func WithTabPrinter(fn HandlerTabPrintFunc) PluginOption {
+func WithTabPrinter(funcs ...HandlerTabPrintFunc) PluginOption {
 	return func(p *Plugin) {
-		p.pluginHandler.HandlerFuncs.PrintTab = fn
+		p.pluginHandler.HandlerFuncs.PrintTabs = funcs
 	}
 }
 
@@ -186,7 +186,7 @@ type HandlerInitRoutesFunc func(router *Router)
 // HandlerFuncs are functions for configuring a plugin.
 type HandlerFuncs struct {
 	Print        HandlerPrinterFunc
-	PrintTab     HandlerTabPrintFunc
+	PrintTabs    []HandlerTabPrintFunc
 	ObjectStatus HandlerObjectStatusFunc
 	HandleAction HandlerActionFunc
 	Navigation   HandlerNavigationFunc
diff --git a/vendor/github.com/iancoleman/strcase/README.md b/vendor/github.com/iancoleman/strcase/README.md
index 5f6391de..7e935eb0 100644
--- a/vendor/github.com/iancoleman/strcase/README.md
+++ b/vendor/github.com/iancoleman/strcase/README.md
@@ -31,3 +31,29 @@ s := "AnyKind of_string"
 ```bash
 go get -u github.com/iancoleman/strcase
 ```
+
+## Custom Acronyms for ToCamel && ToLowerCamel
+
+Often times text can contain specific acronyms which you need to be handled a certain way.
+Out of the box `strcase` treats the string "ID" as "Id" or "id" but there is no way to cater
+for every case in the wild.
+
+To configure your custom acronym globally you can use the following before running any conversion
+
+```go
+import (
+    "github.com/iancoleman/strcase"
+)
+
+func init() {
+    // results in "Api" using ToCamel("API")
+    // results in "api" using ToLowerCamel("API")
+    strcase.ConfigureAcronym("API", "api")
+    
+    // results in "PostgreSQL" using ToCamel("PostgreSQL")
+    // results in "postgreSQL" using ToLowerCamel("PostgreSQL")
+    strcase.ConfigureAcronym("PostgreSQL", "PostgreSQL")
+
+}
+
+```
diff --git a/vendor/github.com/iancoleman/strcase/acronyms.go b/vendor/github.com/iancoleman/strcase/acronyms.go
index ecb533e1..9c3110df 100644
--- a/vendor/github.com/iancoleman/strcase/acronyms.go
+++ b/vendor/github.com/iancoleman/strcase/acronyms.go
@@ -3,3 +3,8 @@ package strcase
 var uppercaseAcronym = map[string]string{
 	"ID": "id",
 }
+
+// ConfigureAcronym allows you to add additional words which will be considered acronyms
+func ConfigureAcronym(key, val string) {
+	uppercaseAcronym[key] = val
+}
diff --git a/vendor/google.golang.org/grpc/encoding/proto/proto.go b/vendor/google.golang.org/grpc/encoding/proto/proto.go
index e1230fdd..3009b35a 100644
--- a/vendor/google.golang.org/grpc/encoding/proto/proto.go
+++ b/vendor/google.golang.org/grpc/encoding/proto/proto.go
@@ -21,6 +21,8 @@
 package proto
 
 import (
+	"fmt"
+
 	"github.com/golang/protobuf/proto"
 	"google.golang.org/grpc/encoding"
 )
@@ -36,11 +38,19 @@ func init() {
 type codec struct{}
 
 func (codec) Marshal(v interface{}) ([]byte, error) {
-	return proto.Marshal(v.(proto.Message))
+	vv, ok := v.(proto.Message)
+	if !ok {
+		return nil, fmt.Errorf("failed to marshal, message is %T, want proto.Message", v)
+	}
+	return proto.Marshal(vv)
 }
 
 func (codec) Unmarshal(data []byte, v interface{}) error {
-	return proto.Unmarshal(data, v.(proto.Message))
+	vv, ok := v.(proto.Message)
+	if !ok {
+		return fmt.Errorf("failed to unmarshal, message is %T, want proto.Message", v)
+	}
+	return proto.Unmarshal(data, vv)
 }
 
 func (codec) Name() string {
diff --git a/vendor/google.golang.org/grpc/health/grpc_health_v1/health_grpc.pb.go b/vendor/google.golang.org/grpc/health/grpc_health_v1/health_grpc.pb.go
index 8c387266..386d16ce 100644
--- a/vendor/google.golang.org/grpc/health/grpc_health_v1/health_grpc.pb.go
+++ b/vendor/google.golang.org/grpc/health/grpc_health_v1/health_grpc.pb.go
@@ -11,6 +11,7 @@ import (
 
 // This is a compile-time assertion to ensure that this generated file
 // is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
 const _ = grpc.SupportPackageIsVersion7
 
 // HealthClient is the client API for Health service.
diff --git a/vendor/google.golang.org/grpc/regenerate.sh b/vendor/google.golang.org/grpc/regenerate.sh
index ed52187d..fc6725b8 100644
--- a/vendor/google.golang.org/grpc/regenerate.sh
+++ b/vendor/google.golang.org/grpc/regenerate.sh
@@ -40,6 +40,9 @@ echo "go install cmd/protoc-gen-go-grpc"
 echo "git clone https://github.com/grpc/grpc-proto"
 git clone --quiet https://github.com/grpc/grpc-proto ${WORKDIR}/grpc-proto
 
+echo "git clone https://github.com/protocolbuffers/protobuf"
+git clone --quiet https://github.com/protocolbuffers/protobuf ${WORKDIR}/protobuf
+
 # Pull in code.proto as a proto dependency
 mkdir -p ${WORKDIR}/googleapis/google/rpc
 echo "curl https://raw.githubusercontent.com/googleapis/googleapis/master/google/rpc/code.proto"
@@ -87,6 +90,7 @@ for src in ${SOURCES[@]}; do
     -I"." \
     -I${WORKDIR}/grpc-proto \
     -I${WORKDIR}/googleapis \
+    -I${WORKDIR}/protobuf/src \
     -I${WORKDIR}/istio \
     ${src}
 done
@@ -97,6 +101,7 @@ for src in ${LEGACY_SOURCES[@]}; do
     -I"." \
     -I${WORKDIR}/grpc-proto \
     -I${WORKDIR}/googleapis \
+    -I${WORKDIR}/protobuf/src \
     -I${WORKDIR}/istio \
     ${src}
 done
diff --git a/vendor/google.golang.org/grpc/rpc_util.go b/vendor/google.golang.org/grpc/rpc_util.go
index ea5bb8d0..c0a1208f 100644
--- a/vendor/google.golang.org/grpc/rpc_util.go
+++ b/vendor/google.golang.org/grpc/rpc_util.go
@@ -888,8 +888,7 @@ type channelzData struct {
 // buffer files to ensure compatibility with the gRPC version used.  The latest
 // support package version is 7.
 //
-// Older versions are kept for compatibility. They may be removed if
-// compatibility cannot be maintained.
+// Older versions are kept for compatibility.
 //
 // These constants should not be referenced from any other code.
 const (
diff --git a/vendor/google.golang.org/grpc/status/status.go b/vendor/google.golang.org/grpc/status/status.go
index 01e182c3..54d18718 100644
--- a/vendor/google.golang.org/grpc/status/status.go
+++ b/vendor/google.golang.org/grpc/status/status.go
@@ -73,9 +73,11 @@ func FromProto(s *spb.Status) *Status {
 	return status.FromProto(s)
 }
 
-// FromError returns a Status representing err if it was produced from this
-// package or has a method `GRPCStatus() *Status`. Otherwise, ok is false and a
-// Status is returned with codes.Unknown and the original error message.
+// FromError returns a Status representing err if it was produced by this
+// package or has a method `GRPCStatus() *Status`.
+// If err is nil, a Status is returned with codes.OK and no message.
+// Otherwise, ok is false and a Status is returned with codes.Unknown and
+// the original error message.
 func FromError(err error) (s *Status, ok bool) {
 	if err == nil {
 		return nil, true
diff --git a/vendor/google.golang.org/grpc/version.go b/vendor/google.golang.org/grpc/version.go
index 685b91c7..51024d6b 100644
--- a/vendor/google.golang.org/grpc/version.go
+++ b/vendor/google.golang.org/grpc/version.go
@@ -19,4 +19,4 @@
 package grpc
 
 // Version is the current grpc version.
-const Version = "1.35.0"
+const Version = "1.36.0"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 03744fab..8924bc06 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -153,7 +153,7 @@ github.com/hashicorp/hcl/json/token
 # github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d
 ## explicit
 github.com/hashicorp/yamux
-# github.com/iancoleman/strcase v0.1.2
+# github.com/iancoleman/strcase v0.1.3
 ## explicit
 github.com/iancoleman/strcase
 # github.com/imdario/mergo v0.3.11
@@ -233,7 +233,7 @@ github.com/uber/jaeger-client-go/thrift
 github.com/uber/jaeger-client-go/thrift-gen/agent
 github.com/uber/jaeger-client-go/thrift-gen/jaeger
 github.com/uber/jaeger-client-go/thrift-gen/zipkincore
-# go.opencensus.io v0.22.6
+# go.opencensus.io v0.23.0
 ## explicit
 go.opencensus.io
 go.opencensus.io/internal
@@ -338,7 +338,7 @@ google.golang.org/appengine/urlfetch
 # google.golang.org/genproto v0.0.0-20201022181438-0ff5f38871d5
 ## explicit
 google.golang.org/genproto/googleapis/rpc/status
-# google.golang.org/grpc v1.35.0
+# google.golang.org/grpc v1.36.0
 ## explicit
 google.golang.org/grpc
 google.golang.org/grpc/attributes


Test patch:
diff --git a/pkg/plugin/grpc_test.go b/pkg/plugin/grpc_test.go
index 65d8b7f6..0dba1016 100644
--- a/pkg/plugin/grpc_test.go
+++ b/pkg/plugin/grpc_test.go
@@ -227,7 +227,7 @@ func Test_GRPCClient_Print(t *testing.T) {
 	})
 }
 
-func Test_GRPCClient_PrintTab(t *testing.T) {
+func Test_GRPCClient_PrintTabs(t *testing.T) {
 	testWithGRPCClient(t, func(mocks *grpcClientMocks) {
 		object := testutil.CreateDeployment("deployment")
 
@@ -244,17 +244,21 @@ func Test_GRPCClient_PrintTab(t *testing.T) {
 		require.NoError(t, err)
 
 		tabResponse := &dashboard.PrintTabResponse{
-			Name:   "tab name",
-			Layout: encodeComponent(t, layout.ToComponent("component title")),
+			Tabs: []*dashboard.PrintTab{
+				{
+					Name:   "tab name",
+					Layout: encodeComponent(t, layout.ToComponent("component title")),
+				},
+			},
 		}
 
 		mocks.protoClient.EXPECT().
-			PrintTab(gomock.Any(), gomock.Eq(objectRequest), grpc.WaitForReady(true)).
+			PrintTabs(gomock.Any(), gomock.Eq(objectRequest), grpc.WaitForReady(true)).
 			Return(tabResponse, nil)
 
 		client := mocks.genClient()
 		ctx := context.Background()
-		got, err := client.PrintTab(ctx, object)
+		got, err := client.PrintTabs(ctx, object)
 		require.NoError(t, err)
 
 		expectedLayout := component.NewFlexLayout("component title")
@@ -265,10 +269,12 @@ func Test_GRPCClient_PrintTab(t *testing.T) {
 					View:  component.NewText("text")},
 			},
 		)
-		expected := plugin.TabResponse{
-			Tab: &component.Tab{
-				Name:     "tab name",
-				Contents: *expectedLayout,
+		expected := []plugin.TabResponse{
+			{
+				Tab: &component.Tab{
+					Name:     "tab name",
+					Contents: *expectedLayout,
+				},
 			},
 		}
 
diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go
index 09ee0e6e..bc7a0165 100644
--- a/pkg/plugin/manager_test.go
+++ b/pkg/plugin/manager_test.go
@@ -166,11 +166,12 @@ func TestManager_Tabs(t *testing.T) {
 
 	store.EXPECT().ClientNames().Return([]string{"plugin1", "plugin2"})
 
-	ch := make(chan component.Tab)
+	ch := make(chan []component.Tab)
 	tabRunner := dashPlugin.DefaultRunner{
 		RunFunc: func(ctx context.Context, name string, gvk schema.GroupVersionKind, object runtime.Object) error {
-			ch <- component.Tab{Name: name}
-
+			ch <- []component.Tab{
+				{Name: name},
+			}
 			return nil
 		},
 	}
diff --git a/pkg/plugin/runner_test.go b/pkg/plugin/runner_test.go
index ec460b8c..a3ff5b61 100644
--- a/pkg/plugin/runner_test.go
+++ b/pkg/plugin/runner_test.go
@@ -162,15 +162,25 @@ func Test_TabRunner(t *testing.T) {
 	store.EXPECT().
 		GetService(gomock.Eq("plugin1")).Return(service, nil)
 
-	tabResponse := plugin.TabResponse{
-		Tab: &component.Tab{},
+	tabResponse := []plugin.TabResponse{
+		{
+			Tab: &component.Tab{},
+		},
+	}
+	tabs := []component.Tab{
+		{
+			Name: "",
+			Contents: component.FlexLayout{
+				Base:   component.Base{},
+				Config: component.FlexLayoutConfig{},
+			},
+		},
 	}
-	tab := component.Tab{}
 
 	service.EXPECT().
-		PrintTab(gomock.Any(), gomock.Eq(object)).Return(tabResponse, nil)
+		PrintTabs(gomock.Any(), gomock.Eq(object)).Return(tabResponse, nil)
 
-	ch := make(chan component.Tab)
+	ch := make(chan []component.Tab)
 	defer close(ch)
 
 	runner := plugin.TabRunner(store, ch)
@@ -178,7 +188,7 @@ func Test_TabRunner(t *testing.T) {
 	done := make(chan bool)
 	go func() {
 		resp := <-ch
-		assert.Equal(t, tab, resp)
+		assert.Equal(t, tabs, resp)
 		done <- true
 	}()
 
diff --git a/pkg/plugin/service/handler_test.go b/pkg/plugin/service/handler_test.go
index c8fc464e..1e97ca18 100644
--- a/pkg/plugin/service/handler_test.go
+++ b/pkg/plugin/service/handler_test.go
@@ -127,7 +127,7 @@ func TestHandler_Print_using_supplied_function(t *testing.T) {
 	assert.True(t, ran)
 }
 
-func TestHandler_PrintTab_default(t *testing.T) {
+func TestHandler_PrintTabs_default(t *testing.T) {
 	controller := gomock.NewController(t)
 	defer controller.Finish()
 
@@ -140,15 +140,14 @@ func TestHandler_PrintTab_default(t *testing.T) {
 	pod := testutil.CreatePod("pod")
 
 	ctx := context.Background()
-	got, err := h.PrintTab(ctx, pod)
+	got, err := h.PrintTabs(ctx, pod)
 	require.NoError(t, err)
 
-	expected := plugin.TabResponse{}
-
-	require.Equal(t, expected, got)
+	expected := []plugin.TabResponse{}
+	assert.Equal(t, expected, got)
 }
 
-func TestHandler_PrintTab_using_supplied_function(t *testing.T) {
+func TestHandler_PrintTabs_using_supplied_function(t *testing.T) {
 	controller := gomock.NewController(t)
 	defer controller.Finish()
 
@@ -161,20 +160,24 @@ func TestHandler_PrintTab_using_supplied_function(t *testing.T) {
 	h := Handler{
 		dashboardClient: dashboardClient,
 		HandlerFuncs: HandlerFuncs{
-			PrintTab: func(r *PrintRequest) (plugin.TabResponse, error) {
-				ran = true
-				assert.Equal(t, dashboardClient, r.DashboardClient)
-				assert.Equal(t, pod, r.Object)
-				return plugin.TabResponse{}, nil
+			PrintTabs: []HandlerTabPrintFunc{
+				func(r *PrintRequest) (plugin.TabResponse, error) {
+					ran = true
+					assert.Equal(t, dashboardClient, r.DashboardClient)
+					assert.Equal(t, pod, r.Object)
+					return plugin.TabResponse{}, nil
+				},
 			},
 		},
 	}
 
 	ctx := context.Background()
-	got, err := h.PrintTab(ctx, pod)
+	got, err := h.PrintTabs(ctx, pod)
 	require.NoError(t, err)
 
-	expected := plugin.TabResponse{}
+	expected := []plugin.TabResponse{
+		{Tab: nil},
+	}
 	assert.Equal(t, expected, got)
 	assert.True(t, ran)
 }