Repository: vmware-tanzu/octant
Pull request #714 (2020-02-20 00:20:50)

Wait for plugin grpc client to be ready before use
Configure GRPC client functions to wait for the server to be ready
before attempting to send requests. This removes a whole class
of `grpc` errors that users may get printed to their console.

This change also removes some unused code and reformats errors.

Fixes: #713

Signed-off-by: bryanl <bryanliles@gmail.com>




Patch:
diff --git a/changelogs/unreleased/213-mklanjsek b/changelogs/unreleased/213-mklanjsek
deleted file mode 100644
index 3aecebfe..00000000
--- a/changelogs/unreleased/213-mklanjsek
+++ /dev/null
@@ -1,1 +0,0 @@
-Added Log search / filter
diff --git a/changelogs/unreleased/667-mklanjsek b/changelogs/unreleased/667-mklanjsek
deleted file mode 100644
index 22206bb5..00000000
--- a/changelogs/unreleased/667-mklanjsek
+++ /dev/null
@@ -1,1 +0,0 @@
-Scroll Log view to the bottom at initial load
diff --git a/changelogs/unreleased/688-bryanl b/changelogs/unreleased/688-bryanl
deleted file mode 100644
index 13011992..00000000
--- a/changelogs/unreleased/688-bryanl
+++ /dev/null
@@ -1,1 +0,0 @@
-Do not show empty CRD versions
diff --git a/changelogs/unreleased/700-mklanjsek b/changelogs/unreleased/700-mklanjsek
deleted file mode 100644
index 49006b32..00000000
--- a/changelogs/unreleased/700-mklanjsek
+++ /dev/null
@@ -1,1 +0,0 @@
-Widened Log timestamp so it doesn't break up to the second line
diff --git a/changelogs/unreleased/702-mklanjsek b/changelogs/unreleased/702-mklanjsek
deleted file mode 100644
index fb84ed49..00000000
--- a/changelogs/unreleased/702-mklanjsek
+++ /dev/null
@@ -1,1 +0,0 @@
-Make filtering (including Regex) case sensitive
diff --git a/changelogs/unreleased/713-bryanl b/changelogs/unreleased/713-bryanl
new file mode 100644
index 00000000..dbfa237f
--- /dev/null
+++ b/changelogs/unreleased/713-bryanl
@@ -0,0 +1,1 @@
+Configure grpc client to wait for server to be ready before sending requests
diff --git a/internal/api/navigation_manager.go b/internal/api/navigation_manager.go
index 85454d09..02a1556a 100644
--- a/internal/api/navigation_manager.go
+++ b/internal/api/navigation_manager.go
@@ -142,8 +142,7 @@ func NavigationGenerator(ctx context.Context, state octant.State, config Navigat
 			contentPath := m.ContentPath()
 			navList, err := m.Navigation(ctx, namespace, contentPath)
 			if err != nil {
-				fmt.Printf("err in navigation: %s", err)
-				return err
+				return fmt.Errorf("unable to generate navigation for module %s: %v", m.Name(), err)
 			}
 
 			mu.Lock()
diff --git a/internal/modules/overview/overview.go b/internal/modules/overview/overview.go
index a20f12c8..2bbb0a3f 100644
--- a/internal/modules/overview/overview.go
+++ b/internal/modules/overview/overview.go
@@ -7,11 +7,15 @@ package overview
 
 import (
 	"context"
+	"fmt"
+	"net/http"
 	"sync"
 
+	"github.com/gorilla/mux"
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 
+	"github.com/vmware-tanzu/octant/internal/api"
 	"github.com/vmware-tanzu/octant/internal/config"
 	"github.com/vmware-tanzu/octant/internal/describer"
 	"github.com/vmware-tanzu/octant/internal/generator"
@@ -291,6 +295,71 @@ func (co *Overview) extensionDescriber(path, namespace string, options describer
 	return extension, nil
 }
 
+func (co *Overview) portForwardsHandler() http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		svc := co.dashConfig.PortForwarder()
+		logger := co.dashConfig.Logger()
+
+		if svc == nil {
+			logger.Errorf("port forward service is nil")
+			http.Error(w, "port forward service is nil", http.StatusInternalServerError)
+			return
+		}
+
+		ctx := log.WithLoggerContext(r.Context(), logger)
+
+		defer func() {
+			if cErr := r.Body.Close(); cErr != nil {
+				logger.With("err", cErr).Errorf("unable to close port forward request body")
+			}
+		}()
+
+		switch r.Method {
+		case http.MethodPost:
+			err := createPortForward(ctx, r.Body, svc, w)
+			handlePortForwardError(w, err, logger)
+		default:
+			api.RespondWithError(
+				w,
+				http.StatusNotFound,
+				fmt.Sprintf("unhandled HTTP method %s", r.Method),
+				logger,
+			)
+		}
+	}
+}
+
+func (co *Overview) portForwardHandler() http.HandlerFunc {
+	logger := co.dashConfig.Logger()
+
+	return func(w http.ResponseWriter, r *http.Request) {
+		svc := co.dashConfig.PortForwarder()
+		if svc == nil {
+			logger.Errorf("port forward service is nil")
+			http.Error(w, "port forward service is nil", http.StatusInternalServerError)
+			return
+		}
+
+		vars := mux.Vars(r)
+		id := vars["id"]
+
+		ctx := log.WithLoggerContext(r.Context(), logger)
+
+		switch r.Method {
+		case http.MethodDelete:
+			err := deletePortForward(ctx, id, co.dashConfig.PortForwarder(), w)
+			handlePortForwardError(w, err, logger)
+		default:
+			api.RespondWithError(
+				w,
+				http.StatusNotFound,
+				fmt.Sprintf("unhandled HTTP method %s", r.Method),
+				logger,
+			)
+		}
+	}
+}
+
 // ActionPaths contain the actions this module is responsible for.
 func (co *Overview) ActionPaths() map[string]action.DispatcherFunc {
 	dispatchers := action.Dispatchers{
diff --git a/internal/printer/customresourcedefinition.go b/internal/printer/customresourcedefinition.go
index 02d817ff..cb0034de 100644
--- a/internal/printer/customresourcedefinition.go
+++ b/internal/printer/customresourcedefinition.go
@@ -43,7 +43,7 @@ func CustomResourceDefinitionHandler(ctx context.Context, crd *unstructured.Unst
 		version := versions[i]
 
 		object.RegisterItems(ItemDescriptor{
-			Func: func() (component.Component, error) {
+			Func: func() (c component.Component, err error) {
 				crGVK, err := gvk.CustomResource(crd, version)
 				if err != nil {
 					return nil, err
@@ -57,17 +57,7 @@ func CustomResourceDefinitionHandler(ctx context.Context, crd *unstructured.Unst
 					return nil, err
 				}
 
-				view, err := CustomResourceListHandler(crd, customResources, version, options.Link)
-				if err != nil {
-					return nil, err
-				}
-
-				if view.IsEmpty() {
-					// if view is empty, return nil so the object printer will skip it.
-					return nil, nil
-				}
-
-				return view, nil
+				return CustomResourceListHandler(crd, customResources, version, options.Link)
 			},
 			Width: component.WidthFull,
 		})
diff --git a/internal/printer/object.go b/internal/printer/object.go
index 21feee5a..9b3cd4b4 100644
--- a/internal/printer/object.go
+++ b/internal/printer/object.go
@@ -246,11 +246,6 @@ func (o *Object) ToComponent(ctx context.Context, options Options) (component.Co
 				return nil, fmt.Errorf("failed to create item view: %w", err)
 			}
 
-			if vc == nil {
-				// don't print nil objects
-				continue
-			}
-
 			if err := section.Add(vc, item.Width); err != nil {
 				return nil, fmt.Errorf("unable to add item to layout section in object printer: %w", err)
 			}
diff --git a/internal/queryer/queryer.go b/internal/queryer/queryer.go
index 6c5d1cbc..c415b74b 100644
--- a/internal/queryer/queryer.go
+++ b/internal/queryer/queryer.go
@@ -292,7 +292,6 @@ func (osq *ObjectStoreQueryer) Children(ctx context.Context, owner *unstructured
 }
 
 var allowed = []schema.GroupVersionKind{
-	gvk.AppReplicaSet,
 	gvk.CronJob,
 	gvk.DaemonSet,
 	gvk.Deployment,
diff --git a/pkg/plugin/api/api.go b/pkg/plugin/api/api.go
index 1a6ed0f7..e9cd4810 100644
--- a/pkg/plugin/api/api.go
+++ b/pkg/plugin/api/api.go
@@ -7,13 +7,13 @@ package api
 
 import (
 	"context"
-	"fmt"
 	"net"
 
 	"github.com/pkg/errors"
+	"google.golang.org/grpc"
+
 	"github.com/vmware-tanzu/octant/internal/log"
 	"github.com/vmware-tanzu/octant/pkg/plugin/api/proto"
-	"google.golang.org/grpc"
 )
 
 // API controlls the dashboard API service.
@@ -59,8 +59,7 @@ func (a *grpcAPI) Start(ctx context.Context) error {
 	logger.Debugf("dashboard plugin api is starting")
 	go func() {
 		if err := s.Serve(a.listener); err != nil {
-			fmt.Println("it broke?", err)
-			logger.Errorf("unable to serve GRPC: %v", err)
+			logger.WithErr(err).Errorf("unable to serve GRPC")
 			return
 		}
 	}()
diff --git a/pkg/plugin/api/client.go b/pkg/plugin/api/client.go
index 6d770472..787ba14e 100644
--- a/pkg/plugin/api/client.go
+++ b/pkg/plugin/api/client.go
@@ -43,7 +43,6 @@ type ClientOption func(c *Client)
 // Client is a dashboard service API client.
 type Client struct {
 	DashboardConnection DashboardConnection
-	// dashboardClientFactory DashboardClientFactory
 }
 
 var _ Service = (*Client)(nil)
diff --git a/pkg/plugin/grpc.go b/pkg/plugin/grpc.go
index 4193a807..e8bb7cfe 100644
--- a/pkg/plugin/grpc.go
+++ b/pkg/plugin/grpc.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/davecgh/go-spew/spew"
 	"github.com/pkg/errors"
+	"google.golang.org/grpc"
 	"google.golang.org/grpc/status"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -55,7 +56,7 @@ func (c *GRPCClient) Content(ctx context.Context, contentPath string) (component
 			Path: contentPath,
 		}
 
-		resp, err := c.client.Content(ctx, req)
+		resp, err := c.client.Content(ctx, req, grpc.WaitForReady(true))
 		if err != nil {
 			return errors.Wrap(err, "grpc client content")
 		}
@@ -86,7 +87,7 @@ func (c *GRPCClient) HandleAction(ctx context.Context, payload action.Payload) e
 			Payload: data,
 		}
 
-		_, err = c.client.HandleAction(ctx, req)
+		_, err = c.client.HandleAction(ctx, req, grpc.WaitForReady(true))
 		if err != nil {
 			if s, isStatus := status.FromError(err); isStatus {
 				return errors.Errorf("grpc error: %s", s.Message())
@@ -111,7 +112,7 @@ func (c *GRPCClient) Navigation(ctx context.Context) (navigation.Navigation, err
 	err := c.run(func() error {
 		req := &dashboard.NavigationRequest{}
 
-		resp, err := c.client.Navigation(ctx, req)
+		resp, err := c.client.Navigation(ctx, req, grpc.WaitForReady(true))
 		if err != nil {
 			return errors.Wrap(err, "grpc client response")
 		}
@@ -137,7 +138,7 @@ func (c *GRPCClient) Register(ctx context.Context, dashboardAPIAddress string) (
 			DashboardAPIAddress: dashboardAPIAddress,
 		}
 
-		resp, err := c.client.Register(ctx, registerRequest)
+		resp, err := c.client.Register(ctx, registerRequest, grpc.WaitForReady(true))
 		if err != nil {
 			spew.Dump(err)
 			return errors.WithMessage(err, "unable to call register function")
@@ -171,7 +172,7 @@ func (c *GRPCClient) ObjectStatus(ctx context.Context, object runtime.Object) (O
 			return err
 		}
 
-		resp, err := c.client.ObjectStatus(ctx, in)
+		resp, err := c.client.ObjectStatus(ctx, in, grpc.WaitForReady(true))
 		if err != nil {
 			return errors.Wrap(err, "grpc client object status")
 		}
@@ -205,7 +206,7 @@ func (c *GRPCClient) Print(ctx context.Context, object runtime.Object) (PrintRes
 			return err
 		}
 
-		resp, err := c.client.Print(ctx, in)
+		resp, err := c.client.Print(ctx, in, grpc.WaitForReady(true))
 		if err != nil {
 			return errors.Wrap(err, "grpc client print")
 		}
@@ -266,7 +267,7 @@ func (c *GRPCClient) PrintTab(ctx context.Context, object runtime.Object) (TabRe
 			return err
 		}
 
-		resp, err := c.client.PrintTab(ctx, in)
+		resp, err := c.client.PrintTab(ctx, in, grpc.WaitForReady(true))
 		if err != nil {
 			return errors.Wrap(err, "grpc client print tab")
 		}
diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go
index ddf4bd69..c057c83d 100644
--- a/pkg/plugin/manager.go
+++ b/pkg/plugin/manager.go
@@ -11,6 +11,7 @@ package plugin
 
 import (
 	"context"
+	"fmt"
 	"os/exec"
 	"path/filepath"
 	"sort"
@@ -453,7 +454,7 @@ func (m *Manager) Print(ctx context.Context, object runtime.Object) (*PrintRespo
 	}()
 
 	if err := runner.Run(ctx, object, m.store.ClientNames()); err != nil {
-		return nil, err
+		return nil, fmt.Errorf("print runner failed: %w", err)
 	}
 	close(ch)
 
diff --git a/pkg/plugin/runner.go b/pkg/plugin/runner.go
index b359ade3..88648549 100644
--- a/pkg/plugin/runner.go
+++ b/pkg/plugin/runner.go
@@ -7,6 +7,7 @@ package plugin
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/pkg/errors"
 	"golang.org/x/sync/errgroup"
@@ -72,7 +73,7 @@ func (pr *DefaultRunner) Run(ctx context.Context, object runtime.Object, clientN
 		fn := func(name string) func() error {
 			return func() error {
 				if err := pr.RunFunc(ctx, name, gvk, object); err != nil {
-					return errors.Wrap(err, "running")
+					return fmt.Errorf("running on %s: %w", name, err)
 				}
 
 				return nil
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.html b/web/src/app/modules/shared/components/smart/logs/logs.component.html
index f385668e..12e3afab 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.html
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.html
@@ -6,38 +6,22 @@
         <option *ngFor="let container of v?.config.containers" value="{{container}}">{{container}}</option>
       </select>
     </clr-select-container>
-    <div class="clr-filter">
-      <div>
-        <label class="clr-control-label">Filter by:</label>
-        <input clrInput class= "clr-control-container" placeholder="no filter" name="input" [(ngModel)]="filterText"/>
-      </div>
-      <div  class="clr-filter-buttons">
-        <button class="btn btn-icon clr-filter-btn" type="button" (click)="onPreviousHighlight()"><</button>
-        <button class="btn btn-icon clr-filter-btn" type="button" (click)="onNextHighlight()">></button>
-      </div>
-    </div>
-
-    <div>
-      <clr-checkbox-wrapper class="timestamp-toggle">
-        <input type="checkbox" clrCheckbox [checked]="showOnlyFiltered" (click)="toggleShowOnlyFiltered()"/>
-        <label>Show only filtered</label>
-      </clr-checkbox-wrapper>
-      <clr-checkbox-wrapper class="timestamp-toggle">
-        <input type="checkbox" clrCheckbox [checked]="shouldDisplayTimestamp" (click)="toggleTimestampDisplay()"/>
-        <label>Display timestamp</label>
-      </clr-checkbox-wrapper>
-    </div>
+    <clr-checkbox-wrapper class="timestamp-toggle">
+      <input type="checkbox" clrCheckbox [checked]="shouldDisplayTimestamp" (click)="toggleTimestampDisplay()"/>
+      <label>Display timestamp</label>
+    </clr-checkbox-wrapper>
   </div>
   <div class="container-logs">
     <div class="container-logs-bg" #scrollTarget (scroll)="onScroll($event)" >
       <ng-container *ngIf="containerLogs?.length < 1">
         No logs
       </ng-container>
-      <div class="container-log code language-bash" *ngFor="let log of filterFunction(containerLogs); trackBy: identifyLog">
-        <div class="container-log-timestamp" *ngIf="shouldDisplayTimestamp"
-             [innerHTML]="'['+highlightText(log.timestamp | date:'long')+']'">
+      <div class="container-log code language-bash" *ngFor="let log of containerLogs; trackBy: identifyLog">
+        <div class="container-log-timestamp" *ngIf="shouldDisplayTimestamp">
+          [{{log.timestamp | date:'long' }}]
         </div>
-        <div class="container-log-message" [innerHTML]="highlightText(log.message)">
+        <div class="container-log-message">
+          {{log.message}}
         </div>
       </div>
     </div>
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.integration.spec.ts b/web/src/app/modules/shared/components/smart/logs/logs.component.integration.spec.ts
index 5513e8bf..5c70dda8 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.integration.spec.ts
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.integration.spec.ts
@@ -44,11 +44,6 @@ describe('LogsComponent <-> PodsLogsService', () => {
   let service: PodLogsService;
   let httpClient: HttpClient;
   let httpTestingController: HttpTestingController;
-  const defaultTestLogs = [
-    { timestamp: '2019-05-06T18:59:06.554540433Z', message: 'messageA' },
-    { timestamp: '2019-05-06T18:59:06.554540433Z', message: 'messageB' },
-    { timestamp: '2019-05-06T18:59:06.554540433Z', message: 'messageC' },
-  ];
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
@@ -93,7 +88,11 @@ describe('LogsComponent <-> PodsLogsService', () => {
 
   it('should allow user to toggle displaying timestamps', () => {
     component.shouldDisplayTimestamp = true;
-    component.containerLogs = defaultTestLogs;
+    component.containerLogs = [
+      { timestamp: '2019-05-06T18:59:06.554540433Z', message: 'messageA' },
+      { timestamp: '2019-05-06T18:59:06.554540433Z', message: 'messageB' },
+      { timestamp: '2019-05-06T18:59:06.554540433Z', message: 'messageC' },
+    ];
     fixture.detectChanges();
 
     let logEntriesDebugElement: DebugElement[] = fixture.debugElement.queryAll(
@@ -104,10 +103,10 @@ describe('LogsComponent <-> PodsLogsService', () => {
       /May \d+, 2019(.+)messageA/
     );
     expect(logEntriesDebugElement[1].nativeElement.textContent).toMatch(
-      /May \d+, 2019(.+)messageB/
+      /May \d+, 2019(.*)+messageB/
     );
     expect(logEntriesDebugElement[2].nativeElement.textContent).toMatch(
-      /May \d+, 2019(.+)messageC/
+      /May \d+, 2019(.*)+messageC/
     );
 
     component.shouldDisplayTimestamp = false;
@@ -117,14 +116,14 @@ describe('LogsComponent <-> PodsLogsService', () => {
       By.css('.container-log')
     );
     expect(logEntriesDebugElement.length).toBe(3);
-    expect(logEntriesDebugElement[0].nativeElement.textContent).toBe(
-      'messageA'
+    expect(logEntriesDebugElement[0].nativeElement.textContent).toMatch(
+      /^\s+messageA\s+$/
     );
-    expect(logEntriesDebugElement[1].nativeElement.textContent).toBe(
-      'messageB'
+    expect(logEntriesDebugElement[1].nativeElement.textContent).toMatch(
+      /^\s+messageB\s+$/
     );
-    expect(logEntriesDebugElement[2].nativeElement.textContent).toBe(
-      'messageC'
+    expect(logEntriesDebugElement[2].nativeElement.textContent).toMatch(
+      /^\s+messageC\s+$/
     );
   });
 
@@ -144,7 +143,7 @@ describe('LogsComponent <-> PodsLogsService', () => {
     expect(logWrapperNativeElement.scrollHeight).toBeGreaterThan(
       logWrapperNativeElement.clientHeight
     );
-    expect(logWrapperNativeElement.scrollTop).toBeGreaterThan(0);
+    expect(logWrapperNativeElement.scrollTop).toBe(0);
 
     const logWrapperHeight = logWrapperNativeElement.clientHeight;
     logWrapperNativeElement.scrollTop = logWrapperNativeElement.scrollHeight;
@@ -185,7 +184,7 @@ describe('LogsComponent <-> PodsLogsService', () => {
     expect(logWrapperNativeElement.scrollHeight).toBeGreaterThan(
       logWrapperNativeElement.clientHeight
     );
-    expect(logWrapperNativeElement.scrollTop).toBeGreaterThan(0);
+    expect(logWrapperNativeElement.scrollTop).toBe(0);
 
     // scroll halfway
     const halfwayScrollMark = Math.floor(
@@ -209,42 +208,7 @@ describe('LogsComponent <-> PodsLogsService', () => {
     expect(logWrapperNativeElement.scrollTop).toBe(halfwayScrollMark);
   });
 
-  it('should filer messages based on search string', () => {
-    component.shouldDisplayTimestamp = true;
-    component.containerLogs = defaultTestLogs;
-    component.filterText = 'message';
-    fixture.detectChanges();
-    VerifyElementsExist('.container-log', 3);
-    VerifyElementsExist('.highlight', 3);
-    VerifyElementsExist('.highlight-selected', 1);
-
-    component.filterText = 'messageA';
-    fixture.detectChanges();
-    VerifyElementsExist('.container-log', 3);
-    VerifyElementsExist('.highlight', 1);
-    VerifyElementsExist('.highlight-selected', 1);
-
-    component.showOnlyFiltered = true;
-    fixture.detectChanges();
-    VerifyElementsExist('.container-log', 1);
-    VerifyElementsExist('.highlight', 1);
-    VerifyElementsExist('.highlight-selected', 1);
-
-    component.filterText = '';
-    fixture.detectChanges();
-    VerifyElementsExist('.container-log', 3);
-    VerifyElementsExist('.highlight', 0);
-    VerifyElementsExist('.highlight-selected', 0);
-  });
-
   afterEach(() => {
     httpTestingController.verify();
   });
-
-  function VerifyElementsExist(selector: string, noItems: number) {
-    const element: DebugElement[] = fixture.debugElement.queryAll(
-      By.css(selector)
-    );
-    expect(element.length).toBe(noItems);
-  }
 });
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.scss b/web/src/app/modules/shared/components/smart/logs/logs.component.scss
index a746c524..046e836a 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.scss
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.scss
@@ -2,40 +2,12 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-.log-actions {
-  display: flex;
-  justify-content: space-between;
-  align-items: baseline;
-}
-
 .app-logs {
   .log-actions,
   .container-select {
-    margin-top: 10px;
-    margin-bottom: 6px;
+    margin-bottom: 20px;
   }
-  .clr-filter {
-    display: flex;
 
-    &-buttons {
-      margin-top: 14px;
-      margin-left: 6px;
-    }
-    &-btn {
-      padding-left: 6px;
-      padding-right: 6px;
-      margin-right: 6px;
-      width: 24px;
-      height: 24px;
-      font-size: 14px;
-      line-height: 16px;
-    }
-  }
-  .log-actions {
-    .clr-form-control {
-      margin-top: 0px;
-    }
-  }
   .container-logs {
     border: 1px solid #ccc;
     border-radius: 4px;
@@ -57,8 +29,8 @@
       flex-direction: row;
 
       &-timestamp {
-        min-width: 294px;
-        width: 294px;
+        min-width: 280px;
+        width: 280px;
         color: #a9b6be;
       }
 
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.spec.ts b/web/src/app/modules/shared/components/smart/logs/logs.component.spec.ts
index ee58225c..1dfb0e93 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.spec.ts
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.spec.ts
@@ -5,8 +5,6 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 import { LogsComponent } from './logs.component';
 import { LogEntry } from 'src/app/modules/shared/models/content';
-import { By } from '@angular/platform-browser';
-import { DebugElement } from '@angular/core';
 
 /**
  * Adds 15 logs to the provided list.
@@ -63,17 +61,4 @@ describe('LogsComponent', () => {
       nativeElement.scrollHeight - nativeElement.offsetHeight
     );
   });
-
-  it('should filter messages based on regex expression', () => {
-    component.filterText = '([A-Z])\\w+';
-    component.shouldDisplayTimestamp = false;
-    component.containerLogs = addLogsToList([]);
-    fixture.detectChanges();
-
-    const selectHighlights: DebugElement[] = fixture.debugElement.queryAll(
-      By.css('.highlight')
-    );
-    expect(selectHighlights.length).toEqual(15);
-    expect(selectHighlights[0].nativeElement.innerText).toEqual('Just');
-  });
 });
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.ts b/web/src/app/modules/shared/components/smart/logs/logs.component.ts
index 772378c3..c59ed292 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.ts
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.ts
@@ -12,7 +12,6 @@ import {
   OnDestroy,
   OnInit,
   ViewChild,
-  ViewEncapsulation,
 } from '@angular/core';
 import {
   LogEntry,
@@ -24,16 +23,11 @@ import {
   PodLogsService,
   PodLogsStreamer,
 } from 'src/app/modules/shared/pod-logs/pod-logs.service';
-import { formatDate } from '@angular/common';
 
 @Component({
   selector: 'app-logs',
-  styles: [
-    '.highlight {color: #ffdf5d} .highlight-selected {background: blue}',
-  ],
   templateUrl: './logs.component.html',
   styleUrls: ['./logs.component.scss'],
-  encapsulation: ViewEncapsulation.None,
 })
 export class LogsComponent implements OnInit, OnDestroy, AfterViewChecked {
   v: LogsView;
@@ -46,7 +40,7 @@ export class LogsComponent implements OnInit, OnDestroy, AfterViewChecked {
   }
 
   private logStream: PodLogsStreamer;
-  scrollToBottom = true;
+  scrollToBottom = false;
 
   private containerLogsDiffer: IterableDiffer<LogEntry>;
   @ViewChild('scrollTarget', { static: true }) scrollTarget: ElementRef;
@@ -54,10 +48,6 @@ export class LogsComponent implements OnInit, OnDestroy, AfterViewChecked {
 
   selectedContainer = '';
   shouldDisplayTimestamp = true;
-  showOnlyFiltered = false;
-  filterText = '';
-  oldFilterText = '';
-  currentSelection = 0;
 
   constructor(
     private podLogsService: PodLogsService,
@@ -87,17 +77,7 @@ export class LogsComponent implements OnInit, OnDestroy, AfterViewChecked {
   }
 
   toggleTimestampDisplay(): void {
-    this.removeHighlightSelection();
-
     this.shouldDisplayTimestamp = !this.shouldDisplayTimestamp;
-    this.scrollToHighlight(0);
-  }
-
-  toggleShowOnlyFiltered(): void {
-    this.removeHighlightSelection();
-
-    this.showOnlyFiltered = !this.showOnlyFiltered;
-    this.scrollToHighlight(0);
   }
 
   startStream() {
@@ -145,10 +125,6 @@ export class LogsComponent implements OnInit, OnDestroy, AfterViewChecked {
       const { nativeElement } = this.scrollTarget;
       nativeElement.scrollTop = nativeElement.scrollHeight;
     }
-    if (this.filterText !== this.oldFilterText) {
-      this.oldFilterText = this.filterText;
-      this.scrollToHighlight(0);
-    }
   }
 
   ngOnDestroy(): void {
@@ -157,95 +133,4 @@ export class LogsComponent implements OnInit, OnDestroy, AfterViewChecked {
       this.logStream = null;
     }
   }
-
-  public highlightText(text: string) {
-    let highlighted = text;
-
-    if (this.filterText) {
-      highlighted = text.replace(new RegExp(this.filterText, 'g'), match => {
-        return '<span class="highlight">' + match + '</span>';
-      });
-    }
-    return `${highlighted}`;
-  }
-
-  public filterFunction(logs: LogEntry[]): LogEntry[] {
-    if (this.showOnlyFiltered) {
-      if (this.shouldDisplayTimestamp) {
-        return logs.filter(
-          log =>
-            log.message.match(new RegExp(this.filterText, 'g')) ||
-            formatDate(log.timestamp, 'long', 'en-US').match(
-              new RegExp(this.filterText, 'g')
-            )
-        );
-      }
-      return logs.filter(log =>
-        log.message.match(new RegExp(this.filterText, 'g'))
-      );
-    }
-
-    return logs;
-  }
-
-  onPreviousHighlight(): void {
-    this.scrollToHighlight(-1);
-  }
-
-  onNextHighlight(): void {
-    this.scrollToHighlight(1);
-  }
-
-  scrollToHighlight(scrollBy: number) {
-    if (scrollBy === 0) {
-      this.currentSelection = 0;
-    }
-
-    if (this.getHighlightedElement(this.currentSelection + scrollBy)) {
-      this.removeHighlightSelection();
-
-      this.currentSelection += scrollBy;
-      const nextSelection: HTMLElement = this.getHighlightedElement(
-        this.currentSelection
-      );
-      const {
-        clientHeight,
-        offsetTop,
-        scrollTop,
-      } = this.scrollTarget.nativeElement;
-      const top = nextSelection.offsetTop - offsetTop;
-
-      if (top > clientHeight + scrollTop || top < scrollTop) {
-        nextSelection.scrollIntoView(true);
-      }
-      nextSelection.className = 'highlight highlight-selected';
-    }
-  }
-
-  removeHighlightSelection(): HTMLElement {
-    const element: HTMLElement = this.getHighlightedElement(
-      this.currentSelection
-    );
-    if (element) {
-      element.className = 'highlight';
-    }
-    return element;
-  }
-
-  getHighlightedElement(index: number): HTMLElement {
-    return document.getElementsByClassName('highlight')[index] as HTMLElement;
-  }
-
-  // totalHighlights(): number {
-  //   return document.getElementsByClassName("highlight").length;
-  // }
-  //
-  // isPreviousDisabled(): boolean {
-  //   return this.currentSelection === 0;
-  // }
-  //
-  // isNextDisabled(): boolean {
-  //   const total = this.totalHighlights();
-  //   return total === 0 || this.currentSelection >= total - 1;
-  // }
 }


Test patch:
diff --git a/internal/printer/object_test.go b/internal/printer/object_test.go
index 369273e5..fe608e26 100644
--- a/internal/printer/object_test.go
+++ b/internal/printer/object_test.go
@@ -204,36 +204,6 @@ func Test_Object_ToComponent(t *testing.T) {
 				},
 			},
 		},
-		{
-			name:   "register items (skip nil)",
-			object: deployment,
-			initFunc: func(o *Object, options *initOptions) {
-				stubPlugins(options.PluginPrinter)
-				o.RegisterItems([]ItemDescriptor{
-					{
-						Func: func() (component.Component, error) {
-							return nil, nil
-						},
-						Width: component.WidthHalf,
-					},
-					{
-						Func: func() (component.Component, error) {
-							return component.NewText("item1"), nil
-						},
-						Width: component.WidthHalf,
-					},
-				}...)
-			},
-			sections: []component.FlexLayoutSection{
-				defaultConfigSection,
-				{
-					{
-						Width: component.WidthHalf,
-						View:  component.NewText("item1"),
-					},
-				},
-			},
-		},
 		{
 			name:   "nil object",
 			object: nil,
diff --git a/pkg/plugin/grpc_test.go b/pkg/plugin/grpc_test.go
index 04c6ddd5..7bfce7a1 100644
--- a/pkg/plugin/grpc_test.go
+++ b/pkg/plugin/grpc_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"google.golang.org/grpc"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -92,7 +93,7 @@ func Test_GRPCClient_Content(t *testing.T) {
 		}
 
 		mocks.protoClient.EXPECT().
-			Content(gomock.Any(), req).
+			Content(gomock.Any(), req, grpc.WaitForReady(true)).
 			Return(resp, nil)
 
 		client := mocks.genClient()
@@ -115,7 +116,7 @@ func Test_GRPCClient_Navigation(t *testing.T) {
 		}
 
 		mocks.protoClient.EXPECT().
-			Navigation(gomock.Any(), req).
+			Navigation(gomock.Any(), req, grpc.WaitForReady(true)).
 			Return(resp, nil)
 
 		client := mocks.genClient()
@@ -147,7 +148,7 @@ func Test_GRPCClient_Register(t *testing.T) {
 			},
 		}
 
-		mocks.protoClient.EXPECT().Register(gomock.Any(), gomock.Any()).Return(resp, nil)
+		mocks.protoClient.EXPECT().Register(gomock.Any(), gomock.Any(), grpc.WaitForReady(true)).Return(resp, nil)
 
 		client := mocks.genClient()
 		apiAddress := "localhost:54321"
@@ -200,7 +201,7 @@ func Test_GRPCClient_Print(t *testing.T) {
 			},
 			Items: itemsData,
 		}
-		mocks.protoClient.EXPECT().Print(gomock.Any(), gomock.Eq(objectRequest)).Return(printResponse, nil)
+		mocks.protoClient.EXPECT().Print(gomock.Any(), gomock.Eq(objectRequest), grpc.WaitForReady(true)).Return(printResponse, nil)
 
 		client := mocks.genClient()
 		ctx := context.Background()
@@ -244,7 +245,7 @@ func Test_GRPCClient_PrintTab(t *testing.T) {
 		}
 
 		mocks.protoClient.EXPECT().
-			PrintTab(gomock.Any(), gomock.Eq(objectRequest)).
+			PrintTab(gomock.Any(), gomock.Eq(objectRequest), grpc.WaitForReady(true)).
 			Return(tabResponse, nil)
 
 		client := mocks.genClient()
@@ -296,7 +297,7 @@ func Test_GRPCClient_ObjectStatus(t *testing.T) {
 			ObjectStatus: statusData,
 		}
 
-		mocks.protoClient.EXPECT().ObjectStatus(gomock.Any(), gomock.Eq(objectRequest)).Return(objectStatusResponse, nil)
+		mocks.protoClient.EXPECT().ObjectStatus(gomock.Any(), gomock.Eq(objectRequest), grpc.WaitForReady(true)).Return(objectStatusResponse, nil)
 
 		client := mocks.genClient()
 		ctx := context.Background()