Repository: vmware-tanzu/octant
Pull request #1403 (2020-09-24 16:25:35)

Fix since creation option in log viewer
Signed-off-by: Wayne Witzel III <wayne@riotousliving.com>

Fixes a bug in master where the `Creation` option in the Log selector was not working.

This was due to a JavaScript number rounding issue since this number exceeded Number.MAX_SAFE_INTEGER
![image](https://user-images.githubusercontent.com/27317/94172826-fc370080-fe60-11ea-9a8e-0cb4cf936695.png)


This was solved by setting the since time to `-1` and using a negative number to trigger looking up the pod creation time and using that to initialize the log streamer.


Patch:
diff --git a/changelogs/CHANGELOG-0.14.md b/changelogs/CHANGELOG-0.14
similarity index 100%
rename from changelogs/CHANGELOG-0.14.md
rename to changelogs/CHANGELOG-0.14
diff --git a/changelogs/CHANGELOG-0.15.md b/changelogs/CHANGELOG-0.15
similarity index 100%
rename from changelogs/CHANGELOG-0.15.md
rename to changelogs/CHANGELOG-0.15
diff --git a/internal/api/container_logs.go b/internal/api/container_logs.go
index 9080e9f0..d03518ab 100644
--- a/internal/api/container_logs.go
+++ b/internal/api/container_logs.go
@@ -32,6 +32,7 @@ type logEntry struct {
 const (
 	RequestPodLogsSubscribe   = "action.octant.dev/podLogs/subscribe"
 	RequestPodLogsUnsubscribe = "action.octant.dev/podLogs/unsubscribe"
+	DefaultSinceSeconds       = 300
 )
 
 type podLogsStateManager struct {
@@ -71,6 +72,7 @@ func (s *podLogsStateManager) StreamPodLogsSubscribe(_ octant.State, payload act
 	if err != nil {
 		return fmt.Errorf("getting namespace from payload: %w", err)
 	}
+
 	podName, err := payload.String("podName")
 	if err != nil {
 		return fmt.Errorf("getting podName from payload: %w", err)
@@ -81,23 +83,25 @@ func (s *podLogsStateManager) StreamPodLogsSubscribe(_ octant.State, payload act
 		return fmt.Errorf("getting containerName from payload: %w", err)
 	}
 
-	since, err := payload.Float64("sinceSeconds")
+	since, err := payload.Int64("sinceSeconds")
 	if err != nil {
 		return fmt.Errorf("getting since from payload: %w", err)
 	}
 
 	// Default to 5 minutes
-	sinceDuration := time.Second * 300
+	sinceSeconds := int64(DefaultSinceSeconds)
+	// Allow negative since, negative since means since creation.
 	if since != 0 {
-		sinceDuration = time.Second * time.Duration(since)
+		sinceSeconds = since
 	}
 
 	eventType := event.NewLoggingEventType(namespace, podName)
+
 	val, ok := s.podLogSubscriptions.Load(eventType)
 	if ok {
 		cancelFn, ok := val.(context.CancelFunc)
 		if !ok {
-			return fmt.Errorf("bad cancelFn conversion for %s", eventType)
+			return fmt.Errorf("bad cancelFn conversion for: %s", eventType)
 		}
 		cancelFn()
 	}
@@ -106,7 +110,7 @@ func (s *podLogsStateManager) StreamPodLogsSubscribe(_ octant.State, payload act
 	key.Name = podName
 	key.Namespace = namespace
 
-	logStreamer, err := container.NewLogStreamer(s.ctx, s.config, key, sinceDuration, containerName)
+	logStreamer, err := container.NewLogStreamer(s.ctx, s.config, key, sinceSeconds, containerName)
 	if err != nil {
 		return fmt.Errorf("creating log streamer: %w", err)
 	}
diff --git a/internal/api/navigation_manager.go b/internal/api/navigation_manager.go
index bc315933..d4a068d9 100644
--- a/internal/api/navigation_manager.go
+++ b/internal/api/navigation_manager.go
@@ -165,7 +165,7 @@ func NavigationGenerator(ctx context.Context, state octant.State, config Navigat
 	return sections, nil
 }
 
-// CreateNavigationEvent creates a navigation event.
+// CreateNavigationEvent creates a namespaces event.
 func CreateNavigationEvent(sections []navigation.Navigation, defaultPath string) oevent.Event {
 	return oevent.Event{
 		Type: oevent.EventTypeNavigation,
diff --git a/internal/modules/overview/container/logstreamer.go b/internal/modules/overview/container/logstreamer.go
index beff545e..1191ab1a 100644
--- a/internal/modules/overview/container/logstreamer.go
+++ b/internal/modules/overview/container/logstreamer.go
@@ -11,9 +11,9 @@ import (
 	"fmt"
 	"io"
 	"sync"
-	"time"
 
 	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/vmware-tanzu/octant/internal/config"
 	"github.com/vmware-tanzu/octant/internal/util/kubernetes"
@@ -25,6 +25,7 @@ type logStreamer struct {
 	pod          string
 	containers   []string
 	sinceSeconds *int64
+	creationTime *v1.Time
 	stream       chan LogEntry
 
 	ctx      context.Context
@@ -36,7 +37,7 @@ type logStreamer struct {
 var _ LogStreamer = (*logStreamer)(nil)
 
 // NewLogStreamer returns an instance of a logStream configured to stream logs for the given namespace/pod/container(s).
-func NewLogStreamer(ctx context.Context, dashConfig config.Dash, key store.Key, sinceDuration time.Duration, containerNames ...string) (*logStreamer, error) {
+func NewLogStreamer(ctx context.Context, dashConfig config.Dash, key store.Key, sinceSeconds int64, containerNames ...string) (*logStreamer, error) {
 	ctx, cancelFn := context.WithCancel(ctx)
 
 	if shouldFetchContainerNames(containerNames) {
@@ -62,13 +63,31 @@ func NewLogStreamer(ctx context.Context, dashConfig config.Dash, key store.Key,
 		}
 	}
 
-	sinceSeconds := int64(sinceDuration.Seconds())
+	var creationTime *v1.Time
+	if sinceSeconds < 0 {
+		object, err := dashConfig.ObjectStore().Get(ctx, key)
+		if err != nil {
+			cancelFn()
+			return nil, fmt.Errorf("getting pod from objectstore: %w", err)
+		}
+
+		var pod corev1.Pod
+		err = kubernetes.FromUnstructured(object, &pod)
+
+		if err != nil {
+			cancelFn()
+			return nil, fmt.Errorf("converting unstructured: %w", err)
+		}
+
+		creationTime = &v1.Time{Time: pod.CreationTimestamp.Time}
+	}
 
 	return &logStreamer{
 		namespace:    key.Namespace,
 		pod:          key.Name,
 		containers:   containerNames,
 		sinceSeconds: &sinceSeconds,
+		creationTime: creationTime,
 		config:       dashConfig,
 		ctx:          ctx,
 		cancelFn:     cancelFn,
@@ -143,11 +162,18 @@ func (s *logStreamer) containerStream(container string) (io.ReadCloser, error) {
 	if err != nil {
 		return nil, err
 	}
-	request := client.CoreV1().Pods(s.namespace).GetLogs(s.pod, &corev1.PodLogOptions{
-		Container:    container,
-		Follow:       true,
-		Timestamps:   true,
-		SinceSeconds: s.sinceSeconds,
-	})
+
+	options := &corev1.PodLogOptions{
+		Container:  container,
+		Follow:     true,
+		Timestamps: true,
+	}
+	if s.creationTime != nil {
+		options.SinceTime = s.creationTime
+	} else {
+		options.SinceSeconds = s.sinceSeconds
+	}
+
+	request := client.CoreV1().Pods(s.namespace).GetLogs(s.pod, options)
 	return request.Stream(s.ctx)
 }
diff --git a/pkg/action/payload.go b/pkg/action/payload.go
index ba138da2..cfb23fca 100644
--- a/pkg/action/payload.go
+++ b/pkg/action/payload.go
@@ -74,6 +74,24 @@ func (p Payload) Uint16(key string) (uint16, error) {
 	return uint16(i), nil
 }
 
+// Int64 returns a int64 from the payload.
+func (p Payload) Int64(key string) (int64, error) {
+	i, found, err := unstructured.NestedFloat64(p, key)
+	if err != nil {
+		return 0, err
+	}
+
+	if !found {
+		return 0, errors.Errorf("payload does not contain %q", key)
+	}
+
+	if i > math.MaxInt64 || i < math.MinInt64 {
+		return 0, errors.Errorf("value %v is not a valid int64", i)
+	}
+
+	return int64(i), nil
+}
+
 // String returns a string from the payload.
 func (p Payload) String(key string) (string, error) {
 	s, ok := p[key].(string)
diff --git a/pkg/view/component/logs.go b/pkg/view/component/logs.go
index cce7a1b0..0913f695 100644
--- a/pkg/view/component/logs.go
+++ b/pkg/view/component/logs.go
@@ -16,7 +16,7 @@ var defaultDurations = []Since{
 	{"1 hour", 3600},
 	{"3 hours", 10800},
 	{"5 hours", 18000},
-	{"Creation", 1<<63 - 1},
+	{"Creation", -1},
 }
 
 type Since struct {
diff --git a/web/src/app/modules/shared/components/presentation/code/code.component.html b/web/src/app/modules/shared/components/presentation/code/code.component.html
index a2e2728f..af365e60 100644
--- a/web/src/app/modules/shared/components/presentation/code/code.component.html
+++ b/web/src/app/modules/shared/components/presentation/code/code.component.html
@@ -5,7 +5,7 @@
     title="Copy to clipboard"
     (click)="copyToClipboard(value)"
   >
-    <clr-icon shape="copy-to-clipboard" size="28"></clr-icon>
+    <clr-icon shape="copy-to-clipboard" size="32"></clr-icon>
   </button>
 </div>
 <pre>
diff --git a/web/src/app/modules/shared/components/presentation/code/code.component.scss b/web/src/app/modules/shared/components/presentation/code/code.component.scss
index 721e8e93..17ea2821 100644
--- a/web/src/app/modules/shared/components/presentation/code/code.component.scss
+++ b/web/src/app/modules/shared/components/presentation/code/code.component.scss
@@ -4,10 +4,6 @@
 
 .code-button-wrapper {
   position: absolute;
-  margin-top: 10px;
+  margin-top: 20px;
   right: 30px;
 }
-
-pre {
-  min-height: 40px;
-}
diff --git a/web/src/app/modules/shared/components/presentation/summary/summary.component.html b/web/src/app/modules/shared/components/presentation/summary/summary.component.html
index 550d3033..5e11bfb4 100644
--- a/web/src/app/modules/shared/components/presentation/summary/summary.component.html
+++ b/web/src/app/modules/shared/components/presentation/summary/summary.component.html
@@ -28,12 +28,7 @@
         <tr *ngFor="let item of v?.config.sections; trackBy: identifyItem">
           <td class="left">{{ item.header }}</td>
           <td class="left">
-            <app-view-table *ngIf="item.content?.metadata?.type === 'table'; else dynamic"
-              [view]="item.content"
-            ></app-view-table>
-            <ng-template #dynamic>
-              <app-view-container [view]="item.content"></app-view-container>
-            </ng-template>
+            <app-view-container [view]="item.content"></app-view-container>
           </td>
         </tr>
       </tbody>
diff --git a/web/src/app/modules/shared/components/smart/editor/editor.component.scss b/web/src/app/modules/shared/components/smart/editor/editor.component.scss
index b8ce41f7..f6a3b574 100644
--- a/web/src/app/modules/shared/components/smart/editor/editor.component.scss
+++ b/web/src/app/modules/shared/components/smart/editor/editor.component.scss
@@ -1,14 +1,6 @@
 /* Copyright (c) 2020 the Octant contributors. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
-:host-context(body) {
-  --editor-border-color: #d8e3e9;
-}
-
-// Color variables for Dark Theme.
-:host-context(body.dark) {
-  --editor-border-color: #324f62;
-}
 
 .editor-container {
   height: 90%;
@@ -18,8 +10,8 @@
 
 .editor {
   flex: 1;
+  height: 0;
   margin: 1.25em 0;
-  border: 1px solid var(--editor-border-color);
 }
 
 ng-monaco-editor {
diff --git a/web/src/app/modules/shared/components/smart/editor/editor.component.ts b/web/src/app/modules/shared/components/smart/editor/editor.component.ts
index 70354fc2..b578b781 100644
--- a/web/src/app/modules/shared/components/smart/editor/editor.component.ts
+++ b/web/src/app/modules/shared/components/smart/editor/editor.component.ts
@@ -48,9 +48,6 @@ export class EditorComponent extends AbstractViewComponent<EditorView> {
     private actionService: ActionService
   ) {
     super();
-
-    this.uri =
-      'file:text-' + Math.random().toString(36).substring(2, 15) + '.yaml';
   }
 
   update() {
@@ -62,6 +59,8 @@ export class EditorComponent extends AbstractViewComponent<EditorView> {
       this.pristineValue = view.config.value;
       this.options.readOnly = view.config.readOnly;
       this.options.language = view.config.language;
+      this.uri =
+        'file:text-' + Math.random().toString(36).substring(2, 15) + '.yaml';
     }
 
     this.submitAction = view.config.submitAction || this.submitAction;
diff --git a/web/src/app/modules/shared/components/smart/helper/helper.component.html b/web/src/app/modules/shared/components/smart/helper/helper.component.html
index 42b1cc8c..537ef016 100644
--- a/web/src/app/modules/shared/components/smart/helper/helper.component.html
+++ b/web/src/app/modules/shared/components/smart/helper/helper.component.html
@@ -26,11 +26,7 @@
   </clr-dropdown>
 </div>
 
-<clr-modal
-  [(clrModalOpen)]="isBuildModalOpen"
-  [clrModalStaticBackdrop]="false"
-  [clrModalSkipAnimation]="true"
->
+<clr-modal [(clrModalOpen)]="isBuildModalOpen" [clrModalStaticBackdrop]="false">
   <h3 class="modal-title">Build Information</h3>
   <div class="modal-body">
     <table class="table table-vertical table-noborder">
@@ -64,7 +60,6 @@
 <clr-modal
   [(clrModalOpen)]="isShortcutModalOpen"
   [clrModalStaticBackdrop]="false"
-  [clrModalSkipAnimation]="true"
 >
   <h3 class="modal-title">Keyboard Shortcuts</h3>
   <div class="modal-body">
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 33bdb7eb..d2d96c77 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
@@ -12,25 +12,25 @@
           *ngFor="let container of v?.config.containers"
           value="{{ container }}"
         >
-          {{ container === '' ? '[all containers]' : container }}</option
-        >
+          {{ container === '' ? '[all containers]' : container }}
+        </option>
       </select>
     </clr-select-container>
     <clr-select-container class="container-select">
       <label>Since</label>
       <select
-              clrSelect
-              name="options"
-              [value]="selectedSince"
-              (change)="onSinceChange($event.target.value)"
+        clrSelect
+        name="options"
+        [value]="selectedSince"
+        (change)="onSinceChange($event.target.value)"
       >
         <option
-                *ngFor="let duration of v?.config.durations"
-                label="{{ duration.label }}"
-                value="{{ duration.seconds }}"
-        >
-          {{ container === '' ? '[all containers]' : container }}</option
+          *ngFor="let duration of v?.config.durations"
+          label="{{ duration.label }}"
+          value="{{ duration.seconds }}"
         >
+          {{ container === '' ? '[all containers]' : container }}
+        </option>
       </select>
     </clr-select-container>
     <div class="clr-filter">
@@ -93,9 +93,7 @@
   </div>
   <div class="container-logs">
     <div class="container-logs-bg" #scrollTarget (scroll)="onScroll($event)">
-      <ng-container *ngIf="containerLogs?.length < 1">
-        No logs
-      </ng-container>
+      <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"
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 74da1d3f..afc3f971 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
@@ -77,8 +77,8 @@ export class LogsComponent
     }
   }
 
-  onSinceChange(sinceSelection: number): void {
-    this.selectedSince = sinceSelection;
+  onSinceChange(selectedSince: string): void {
+    this.selectedSince = +selectedSince;
     this.stopStreamIfStarted();
     this.startStream();
   }
diff --git a/web/src/app/modules/shared/services/content/content.service.ts b/web/src/app/modules/shared/services/content/content.service.ts
index 761f2b99..889e8a2a 100644
--- a/web/src/app/modules/shared/services/content/content.service.ts
+++ b/web/src/app/modules/shared/services/content/content.service.ts
@@ -35,6 +35,7 @@ const emptyContentResponse: ContentResponse = {
   providedIn: 'root',
 })
 export class ContentService {
+  defaultPath = new BehaviorSubject<string>('');
   current = new BehaviorSubject<ContentResponse>(emptyContentResponse);
   viewScrollPos = new BehaviorSubject<number>(0);
   debouncedScrollPos = new BehaviorSubject<number>(0);
diff --git a/web/src/app/modules/shared/services/namespace/namespace.service.ts b/web/src/app/modules/shared/services/namespace/namespace.service.ts
index e9d7c554..decac872 100644
--- a/web/src/app/modules/shared/services/namespace/namespace.service.ts
+++ b/web/src/app/modules/shared/services/namespace/namespace.service.ts
@@ -41,11 +41,9 @@ export class NamespaceService {
     });
 
     this.activeNamespace.subscribe(namespace => {
-      if (namespace.length > 0) {
-        websocketService.sendMessage('action.octant.dev/setNamespace', {
-          namespace,
-        });
-      }
+      websocketService.sendMessage('action.octant.dev/setNamespace', {
+        namespace,
+      });
     });
 
     this.notifierSession = this.notifierService.createSession();
diff --git a/web/src/app/modules/shared/services/navigation/navigation.service.spec.ts b/web/src/app/modules/shared/services/navigation/navigation.service.spec.ts
index 30e6cee6..783c528b 100644
--- a/web/src/app/modules/shared/services/navigation/navigation.service.spec.ts
+++ b/web/src/app/modules/shared/services/navigation/navigation.service.spec.ts
@@ -50,6 +50,9 @@ describe('NavigationService', () => {
 
         backendService.triggerHandler('event.octant.dev/navigation', update);
         svc.current.subscribe(current => expect(current).toEqual(update));
+        contentService.defaultPath.subscribe(defaultPath =>
+          expect(defaultPath).toEqual(update.defaultPath)
+        );
       }
     ));
 
diff --git a/web/src/app/modules/shared/services/navigation/navigation.service.ts b/web/src/app/modules/shared/services/navigation/navigation.service.ts
index 9a1b55d2..92049e9e 100644
--- a/web/src/app/modules/shared/services/navigation/navigation.service.ts
+++ b/web/src/app/modules/shared/services/navigation/navigation.service.ts
@@ -11,7 +11,6 @@ import { ContentService } from '../content/content.service';
 import { NavigationEnd, Router, RouterEvent } from '@angular/router';
 import { filter } from 'rxjs/operators';
 import { LoadingService } from '../loading/loading.service';
-import { NamespaceService } from '../namespace/namespace.service';
 
 const emptyNavigation: Navigation = {
   sections: [],
@@ -41,6 +40,8 @@ export class NavigationService {
     websocketService.registerHandler('event.octant.dev/navigation', data => {
       const update = data as Navigation;
       this.current.next(update);
+
+      contentService.defaultPath.next(update.defaultPath);
       this.updateLastSelection();
     });
 
diff --git a/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.html b/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.html
index 0bd047c5..6bc92240 100644
--- a/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.html
@@ -3,13 +3,7 @@
   <span>Apply YAML</span>
 </div>
 
-<clr-modal
-  [(clrModalOpen)]="isOpen"
-  [clrModalSize]="'xl'"
-  [clrModalClosable]="true"
-  [clrModalStaticBackdrop]="false"
-  [clrModalSkipAnimation]="true"
->
+<clr-modal [(clrModalOpen)]="isOpen" [clrModalSize]="'xl'" [clrModalClosable]="true" [clrModalStaticBackdrop]="false">
   <h3 class="modal-title">Apply YAML</h3>
   <div class="modal-body">
       <app-view-editor [view]="editorView"></app-view-editor>
diff --git a/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.scss b/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.scss
index 57995474..b3feb706 100644
--- a/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.scss
@@ -3,8 +3,6 @@
  */
 
 .modal-body {
-  overflow-y: hidden;
-
   ::ng-deep ng-monaco-editor {
     height: calc(60vh - 100px);
   }
diff --git a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.html b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.html
index 3f0a0b24..25187b2a 100644
--- a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.html
@@ -1,5 +1,5 @@
 <div class="app-namespace-selector">
-  <ng-template [ngIf]="showDropdown" [ngIfElse]="hideDropdown">
+  <ng-template [ngIf]="showDropdown()" [ngIfElse]="hideDropdown">
     <clr-dropdown class="dropdown-top" [clrCloseMenuOnItemClick]="true">
       <button type="button" class="dropdown-button" clrDropdownTrigger>
         <clr-icon shape="namespace"></clr-icon>
@@ -21,7 +21,6 @@
             [ngClass]="namespaceClass(namespace)"
             clrDropdownItem
             (click)="selectNamespace(namespace)"
-            [routerLink]="'/overview/namespace/' + namespace"
           >
             {{ namespace }}
           </button>
diff --git a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.ts b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.ts
index 528fcc3c..a49afdd9 100644
--- a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.ts
@@ -2,13 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import {
-  ChangeDetectionStrategy,
-  ChangeDetectorRef,
-  Component,
-  OnDestroy,
-  OnInit,
-} from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
 import { NamespaceService } from 'src/app/modules/shared/services/namespace/namespace.service';
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
 import { Subscription } from 'rxjs';
@@ -18,7 +12,6 @@ import { NavigationService } from '../../../../shared/services/navigation/naviga
   selector: 'app-namespace',
   templateUrl: './namespace.component.html',
   styleUrls: ['./namespace.component.scss'],
-  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class NamespaceComponent implements OnInit, OnDestroy {
   namespaces: string[];
@@ -30,46 +23,36 @@ export class NamespaceComponent implements OnInit, OnDestroy {
   };
   lastSelection: number;
   activeUrl: string;
-  showDropdown: boolean;
 
   private namespaceSubscription: Subscription;
 
   constructor(
     private namespaceService: NamespaceService,
-    private navigationService: NavigationService,
-    private cdr: ChangeDetectorRef
+    private navigationService: NavigationService
   ) {}
 
   ngOnInit() {
     this.namespaceSubscription = this.namespaceService.activeNamespace.subscribe(
       (namespace: string) => {
         this.currentNamespace = namespace;
-        this.cdr.detectChanges();
       }
     );
 
     this.namespaceSubscription = this.namespaceService.availableNamespaces.subscribe(
       (namespaces: string[]) => {
         this.namespaces = namespaces;
-        this.cdr.detectChanges();
       }
     );
 
-    this.navigationService.current.subscribe(navigation => {
-      this.navigation = navigation;
-      this.cdr.detectChanges();
-    });
-
-    this.navigationService.activeUrl.subscribe(url => {
-      this.activeUrl = url;
-      this.cdr.detectChanges();
-    });
-
-    this.navigationService.lastSelection.subscribe(selection => {
-      this.lastSelection = selection;
-      this.showDropdown = this.hasDropdown();
-      this.cdr.detectChanges();
-    });
+    this.navigationService.current.subscribe(
+      navigation => (this.navigation = navigation)
+    );
+
+    this.navigationService.lastSelection.subscribe(
+      selection => (this.lastSelection = selection)
+    );
+
+    this.navigationService.activeUrl.subscribe(url => (this.activeUrl = url));
   }
 
   ngOnDestroy(): void {
@@ -85,7 +68,7 @@ export class NamespaceComponent implements OnInit, OnDestroy {
     this.namespaceService.setNamespace(namespace);
   }
 
-  hasDropdown(): boolean {
+  showDropdown() {
     if (this.lastSelection && this.navigation.sections[this.lastSelection]) {
       const url = this.navigation.sections[this.lastSelection].path;
       return !this.isClusterSpecific(url);
@@ -93,32 +76,27 @@ export class NamespaceComponent implements OnInit, OnDestroy {
     return true;
   }
 
-  private namespaceFromUrl(url: string): string {
-    if (url) {
-      const paths = url.split('/');
-      const len = paths.length;
-      if (len > 1 && paths[len - 2] === 'namespaces') {
-        const hashIndex = paths[len - 1].indexOf('#');
-        return hashIndex > 0
-          ? paths[len - 1].substring(0, hashIndex)
-          : paths[len - 1];
-      }
+  private namespaceFromUrl(): string {
+    const paths = this.activeUrl.split('/');
+    const len = paths.length;
+    if (len > 1 && paths[len - 2] === 'namespaces') {
+      const hashIndex = paths[len - 1].indexOf('#');
+      return hashIndex > 0
+        ? paths[len - 1].substring(0, hashIndex)
+        : paths[len - 1];
     }
     return '';
   }
 
   private isClusterSpecific(url: string) {
-    const ns = this.namespaceFromUrl(url);
+    const ns = this.namespaceFromUrl();
     if (ns.length > 0) {
       this.selectNamespace(ns);
       return false;
     }
 
     if (url.includes('cluster-overview')) {
-      return (
-        this.currentNamespace.length === 0 ||
-        !url.endsWith(this.currentNamespace)
-      );
+      return !this.activeUrl.endsWith(this.currentNamespace);
     }
 
     return false;
diff --git a/web/src/app/modules/sugarloaf/components/smart/quick-switcher/quick-switcher.component.scss b/web/src/app/modules/sugarloaf/components/smart/quick-switcher/quick-switcher.component.scss
index d6391f04..752bcb03 100644
--- a/web/src/app/modules/sugarloaf/components/smart/quick-switcher/quick-switcher.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/quick-switcher/quick-switcher.component.scss
@@ -69,10 +69,6 @@
     color: var(--destinationActive-color);
   }
 
-  .modal-body {
-    overflow-y: hidden;
-  }
-
   .modal-footer {
     padding-top: 10px;
     justify-content: flex-start;


Test patch:
diff --git a/pkg/action/payload_test.go b/pkg/action/payload_test.go
index 6231a4cc..90a2b253 100644
--- a/pkg/action/payload_test.go
+++ b/pkg/action/payload_test.go
@@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
 package action
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -238,6 +239,61 @@ func TestPayload_Uint16(t *testing.T) {
 	}
 }
 
+func TestPayload_Int64(t *testing.T) {
+	tests := []struct {
+		name     string
+		payload  Payload
+		key      string
+		isErr    bool
+		expected int64
+	}{
+		{
+			name:     "source is int",
+			payload:  Payload{"int64": float64(7)},
+			key:      "int64",
+			expected: int64(7),
+		},
+		{
+			name:    "source overflows",
+			payload: Payload{"int64": float64(1 << 64)},
+			key:     "int64",
+			isErr:   true,
+		},
+		{
+			name:    "source overflows",
+			payload: Payload{"int64": float64(-1 << 64)},
+			key:     "int64",
+			isErr:   true,
+		},
+		{
+			name:    "value is not int",
+			payload: Payload{"int64": true},
+			key:     "int64",
+			isErr:   true,
+		},
+		{
+			name:    "key does not exist",
+			payload: Payload{},
+			key:     "invalid",
+			isErr:   true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			got, err := test.payload.Int64(test.key)
+			if test.isErr {
+				require.Error(t, err)
+				fmt.Println(got, err)
+				return
+			}
+			require.NoError(t, err)
+
+			assert.Equal(t, test.expected, got)
+		})
+	}
+}
+
 func TestPayload_Raw(t *testing.T) {
 	type args struct {
 		key string