Repository: vmware-tanzu/octant
Pull request #812 (2020-04-03 00:08:37)

Vertical Navigation 
After ironing a few layout issues with @rossfenrick, I think this is a got point to be reviewed and merged to master. Vertical navigation now works for both expanded and collapsed state with the hand-rolled flyout menu. Unfortunately, there was some history lost during merge from the old branch due to so many changes since that work started.  

@vmware-tanzu/octant-reviewers feedback please!

GuessWhoSamFoo: As discussed in slack, we should merge this to master and break apart pending issues into an epic with the main one being losing a level of hierarchy for plugins.
mklanjsek: All good points, thanks @GuessWhoSamFoo!
rossfenrick: >  The sidebar should be collapsed by default. (we may even consider not allowing expansion at all later)

I'd highly recommend against this. A widely accepted UX best practice is that primary navigation should only be hidden if it the viewport is constrained by size (mobile).
bryanl: @rossfenrick The idea is to make it act more like vscode's sidebar. It would not be hidden, but would instead take up less room.
rossfenrick: @bryanl Sorry, when I say "hidden" I mean collapsed/only icons. Something we could change is if the user chooses to collapse the navigation, it stays like that on refresh and the next time they start the app. 

What's the benefit to removing the ability to expand & collapse?
mklanjsek: Expanded/collapsed state should certainly be a user level setting stored in browser local storage, similar to theme and other preferences.

As for default state, expanded state is certainly geared towards new and novice users, so from that point of view it's probably a good choice to be a default state. If we can remember that state once a user flips the switch, we should be able to handle wide spectrum of user affinities. 
wwitzel3: I see lots of tools and other products that don't even offer an expanded state. They use tooltips / etc.. and our flyout menu + tooltip provide all of the information a person needs to navigate and they have a set of medium/large icons for the navigation.

I really like the idea of moving away from the expanded navigation all together, similar to tools like VS code. Making the collapsed view the only view and making the icons bigger.
mklanjsek: I suggest we switch to collapsed, test that internally and make final decision before the next release.  Anybody against that? 
mklanjsek: I addressed all above issues, except this one:

> Namespace Overview and Cluster Overview both just read Overview and they are ambiguous when the menu is collapsed.

Unfortunately client does not have enough info to show that in tooltip. Change will be required on backend to add tooltip text, so I'll open a separate issue to handle that..    

Patch:
diff --git a/README.md b/README.md
index 65730515..83a8e3f1 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ Feature requests, bug reports, and enhancements are welcome. Contributors, maint
 
 ## Contributing
 
-Contributors will need to sign a DCO (Developer Certificate of Origin) with all changes. We also ask that a changelog entry is included with your pull request. Details are described in our [contributing](CONTRIBUTING.md) documentation.
+New contributors will need to sign a CLA (contributor license agreement). We also ask that a changelog entry is included with your pull request. Details are described in our [contributing](CONTRIBUTING.md) documentation.
 
 See our [hacking](HACKING.md) guide for getting your development environment setup.
 
diff --git a/build.go b/build.go
index a20444c9..2d5ab307 100644
--- a/build.go
+++ b/build.go
@@ -1,5 +1,3 @@
-// +build ignore
-
 /*
 Copyright (c) 2019 the Octant contributors. All Rights Reserved.
 SPDX-License-Identifier: Apache-2.0
diff --git a/changelogs/unreleased/817-GuessWhoSamFoo b/changelogs/unreleased/817-GuessWhoSamFoo
deleted file mode 100644
index 35f3adc0..00000000
--- a/changelogs/unreleased/817-GuessWhoSamFoo
+++ /dev/null
@@ -1,1 +0,0 @@
-Added support for different persistent volume sources in summary view
diff --git a/doc.go b/doc.go
index fca4af44..56cc5323 100644
--- a/doc.go
+++ b/doc.go
@@ -1,3 +1,1 @@
-// +build ignore
-
 package octant
diff --git a/internal/api/api.go b/internal/api/api.go
index f7eb7727..e6873bbf 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -71,7 +71,7 @@ func serveAsJSON(w http.ResponseWriter, v interface{}, logger log.Logger) {
 
 // Service is an API service.
 type Service interface {
-	Handler(ctx context.Context) (http.Handler, error)
+	Handler(ctx context.Context) (*mux.Router, error)
 	ForceUpdate() error
 }
 
@@ -143,7 +143,7 @@ func (a *API) ForceUpdate() error {
 }
 
 // Handler returns a HTTP handler for the service.
-func (a *API) Handler(ctx context.Context) (http.Handler, error) {
+func (a *API) Handler(ctx context.Context) (*mux.Router, error) {
 	router := mux.NewRouter()
 	router.Use(rebindHandler(ctx, acceptedHosts()))
 
diff --git a/internal/api/fake/mock_service.go b/internal/api/fake/mock_service.go
index 9d5f40f0..af2f1afa 100644
--- a/internal/api/fake/mock_service.go
+++ b/internal/api/fake/mock_service.go
@@ -7,7 +7,7 @@ package fake
 import (
 	context "context"
 	gomock "github.com/golang/mock/gomock"
-	http "net/http"
+	mux "github.com/gorilla/mux"
 	reflect "reflect"
 )
 
@@ -49,10 +49,10 @@ func (mr *MockServiceMockRecorder) ForceUpdate() *gomock.Call {
 }
 
 // Handler mocks base method
-func (m *MockService) Handler(arg0 context.Context) (http.Handler, error) {
+func (m *MockService) Handler(arg0 context.Context) (*mux.Router, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Handler", arg0)
-	ret0, _ := ret[0].(http.Handler)
+	ret0, _ := ret[0].(*mux.Router)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
diff --git a/internal/modules/clusteroverview/clusteroverview.go b/internal/modules/clusteroverview/clusteroverview.go
index 05bab279..b8ad4a35 100644
--- a/internal/modules/clusteroverview/clusteroverview.go
+++ b/internal/modules/clusteroverview/clusteroverview.go
@@ -183,23 +183,34 @@ func (co *ClusterOverview) Navigation(ctx context.Context, namespace string, roo
 			"Custom Resources":   "custom-resources",
 			"RBAC":               "rbac",
 			"Nodes":              "nodes",
-			"Persistent Volumes": "persistent-volumes",
+			"Storage":            "storage",
 			"Port Forwards":      "port-forward",
 		},
 		EntriesFuncs: map[string]octant.EntriesFunc{
+			"Overview":           nil,
 			"Namespaces":         nil,
 			"Custom Resources":   navigation.CRDEntries,
 			"RBAC":               rbacEntries,
 			"Nodes":              nil,
-			"Persistent Volumes": nil,
+			"Storage":            storageEntries,
 			"Port Forwards":      nil,
 		},
+		IconMap: map[string]string{
+			"Overview":         icon.Overview,
+			"Namespaces":       icon.Namespaces,
+			"Custom Resources": icon.CustomResources,
+			"RBAC":             icon.RBAC,
+			"Nodes":            icon.Nodes,
+			"Storage":          icon.ConfigAndStorage,
+			"Port Forwards":    icon.PortForwards,
+		},
 		Order: []string{
+			"Overview",
 			"Namespaces",
 			"Custom Resources",
 			"RBAC",
 			"Nodes",
-			"Persistent Volumes",
+			"Storage",
 			"Port Forwards",
 		},
 	}
@@ -208,14 +219,12 @@ func (co *ClusterOverview) Navigation(ctx context.Context, namespace string, roo
 
 	nf := octant.NewNavigationFactory("", root, objectStore, navigationEntries)
 
-	entries, err := nf.Generate(ctx, "Cluster Overview", icon.ClusterOverview, "", true)
+	entries, err := nf.Generate(ctx, "Cluster", true)
 	if err != nil {
 		return nil, err
 	}
 
-	return []navigation.Navigation{
-		*entries,
-	}, nil
+	return entries, nil
 }
 
 func (co *ClusterOverview) SetNamespace(namespace string) error {
@@ -236,12 +245,27 @@ func (co *ClusterOverview) Generators() []octant.Generator {
 
 func rbacEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, _ bool) ([]navigation.Navigation, bool, error) {
 	neh := navigation.EntriesHelper{}
-	neh.Add("Cluster Roles", "cluster-roles", icon.ClusterOverviewClusterRole,
+	neh.Add("Overview", "", false)
+	neh.Add("Cluster Roles", "cluster-roles",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.ClusterRole), objectStore))
-	neh.Add("Cluster Role Bindings", "cluster-role-bindings", icon.ClusterOverviewClusterRoleBinding,
+	neh.Add("Cluster Role Bindings", "cluster-role-bindings",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.ClusterRoleBinding), objectStore))
 
-	children, err := neh.Generate(prefix)
+	children, err := neh.Generate(prefix, namespace, "")
+	if err != nil {
+		return nil, false, err
+	}
+
+	return children, false, nil
+}
+
+func storageEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, _ bool) ([]navigation.Navigation, bool, error) {
+	neh := navigation.EntriesHelper{}
+	neh.Add("Overview", "", false)
+	neh.Add("Persistent Volumes", "persistent-volumes",
+		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.PersistentVolume), objectStore))
+
+	children, err := neh.Generate(prefix, namespace, "")
 	if err != nil {
 		return nil, false, err
 	}
diff --git a/internal/modules/clusteroverview/objects.go b/internal/modules/clusteroverview/objects.go
index bebe6a37..8cb4d35b 100644
--- a/internal/modules/clusteroverview/objects.go
+++ b/internal/modules/clusteroverview/objects.go
@@ -58,8 +58,8 @@ var (
 		IconName:              icon.ClusterOverviewNode,
 	})
 
-	persistentVolumeDescriber = describer.NewResource(describer.ResourceOptions{
-		Path:           "/persistent-volumes",
+	storagePersistentVolumeDescriber = describer.NewResource(describer.ResourceOptions{
+		Path:           "/storage/persistent-volumes",
 		ObjectStoreKey: store.Key{APIVersion: "v1", Kind: "PersistentVolume"},
 		ListType:       &v1.PersistentVolumeList{},
 		ObjectType:     &v1.PersistentVolume{},
@@ -68,6 +68,12 @@ var (
 		IconName:       icon.ClusterOverviewPersistentVolume,
 	})
 
+	storageDescriber = describer.NewSection(
+		"/storage",
+		"Storage",
+		storagePersistentVolumeDescriber,
+	)
+
 	namespacesDescriber = describer.NewResource(describer.ResourceOptions{
 		Path:                  "/namespaces",
 		ObjectStoreKey:        store.Key{APIVersion: "v1", Kind: "Namespace"},
@@ -88,7 +94,7 @@ var (
 		customResourcesDescriber,
 		rbacDescriber,
 		nodesDescriber,
-		persistentVolumeDescriber,
+		storageDescriber,
 		portForwardDescriber,
 	)
 )
diff --git a/internal/modules/clusteroverview/path.go b/internal/modules/clusteroverview/path.go
index af47df87..89353586 100644
--- a/internal/modules/clusteroverview/path.go
+++ b/internal/modules/clusteroverview/path.go
@@ -40,7 +40,7 @@ func gvkPath(namespace, apiVersion, kind, name string) (string, error) {
 	case apiVersion == "v1" && kind == "Node":
 		p = "/nodes"
 	case apiVersion == "v1" && kind == "PersistentVolume":
-		p = "/persistent-volumes"
+		p = "/storage/persistent-volumes"
 	default:
 		return "", errors.Errorf("unknown object %s %s", apiVersion, kind)
 	}
diff --git a/internal/modules/configuration/configuration.go b/internal/modules/configuration/configuration.go
index ed6a7804..f387923f 100644
--- a/internal/modules/configuration/configuration.go
+++ b/internal/modules/configuration/configuration.go
@@ -95,16 +95,10 @@ func (c *Configuration) ContentPath() string {
 func (c *Configuration) Navigation(ctx context.Context, namespace, root string) ([]navigation.Navigation, error) {
 	return []navigation.Navigation{
 		{
-			Title:    "Configuration",
-			Path:     path.Join(c.ContentPath(), "/"),
-			IconName: icon.Configuration,
-			Children: []navigation.Navigation{
-				{
-					Title:    "Plugins",
-					Path:     path.Join(c.ContentPath(), "plugins"),
-					IconName: icon.ConfigurationPlugin,
-				},
-			},
+			Module:   "Configuration",
+			Title:    "Plugin",
+			Path:     path.Join(c.ContentPath(), "plugins"),
+			IconName: icon.ConfigurationPlugin,
 		},
 	}, nil
 }
diff --git a/internal/modules/overview/navigation.go b/internal/modules/overview/navigation.go
index 5d2a477a..7a1c5454 100644
--- a/internal/modules/overview/navigation.go
+++ b/internal/modules/overview/navigation.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/vmware-tanzu/octant/internal/gvk"
 	"github.com/vmware-tanzu/octant/internal/loading"
-	"github.com/vmware-tanzu/octant/pkg/icon"
 	"github.com/vmware-tanzu/octant/pkg/navigation"
 	"github.com/vmware-tanzu/octant/pkg/store"
 )
@@ -29,24 +28,26 @@ var (
 func workloadEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, _ bool) ([]navigation.Navigation, bool, error) {
 	neh := navigation.EntriesHelper{}
 
-	neh.Add("Cron Jobs", "cron-jobs", icon.OverviewCronJob,
+	neh.Add("Overview", "", false)
+	neh.Add("Cron Jobs", "cron-jobs",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.CronJob), objectStore))
-	neh.Add("Daemon Sets", "daemon-sets", icon.OverviewDaemonSet,
+	neh.Add("Daemon Sets", "daemon-sets",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.DaemonSet), objectStore))
-	neh.Add("Deployments", "deployments", icon.OverviewDeployment,
+	neh.Add("Deployments", "deployments",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.Deployment), objectStore))
-	neh.Add("Jobs", "jobs", icon.OverviewJob,
+	neh.Add("Jobs", "jobs",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.Job), objectStore))
-	neh.Add("Pods", "pods", icon.OverviewPod,
+	neh.Add("Pods", "pods",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.Pod), objectStore))
-	neh.Add("Replica Sets", "replica-sets", icon.OverviewReplicaSet,
+	neh.Add("Replica Sets", "replica-sets",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.ExtReplicaSet), objectStore))
-	neh.Add("Replication Controllers", "replication-controllers", icon.OverviewReplicationController,
+	neh.Add("Replication Controllers", "replication-controllers",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.ReplicationController), objectStore))
-	neh.Add("Stateful Sets", "stateful-sets", icon.OverviewStatefulSet,
+	neh.Add("Stateful Sets", "stateful-sets",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.StatefulSet), objectStore))
 
-	children, err := neh.Generate(prefix)
+	children, err := neh.Generate(prefix, namespace, "")
+
 	if err != nil {
 		return nil, false, err
 	}
@@ -56,14 +57,15 @@ func workloadEntries(ctx context.Context, prefix, namespace string, objectStore
 
 func discoAndLBEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, _ bool) ([]navigation.Navigation, bool, error) {
 	neh := navigation.EntriesHelper{}
-	neh.Add("Horizontal Pod Autoscalers", "horizontal-pod-autoscalers", icon.OverviewHorizontalPodAutoscaler,
+	neh.Add("Overview", "", false)
+	neh.Add("Horizontal Pod Autoscalers", "horizontal-pod-autoscalers",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.HorizontalPodAutoscaler), objectStore))
-	neh.Add("Ingresses", "ingresses", icon.OverviewIngress,
+	neh.Add("Ingresses", "ingresses",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.Ingress), objectStore))
-	neh.Add("Services", "services", icon.OverviewService,
+	neh.Add("Services", "services",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.Service), objectStore))
 
-	children, err := neh.Generate(prefix)
+	children, err := neh.Generate(prefix, namespace, "")
 	if err != nil {
 		return nil, false, err
 	}
@@ -73,16 +75,18 @@ func discoAndLBEntries(ctx context.Context, prefix, namespace string, objectStor
 
 func configAndStorageEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, _ bool) ([]navigation.Navigation, bool, error) {
 	neh := navigation.EntriesHelper{}
-	neh.Add("Config Maps", "config-maps", icon.OverviewConfigMap,
+
+	neh.Add("Overview", "", false)
+	neh.Add("Config Maps", "config-maps",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.ConfigMap), objectStore))
-	neh.Add("Persistent Volume Claims", "persistent-volume-claims", icon.OverviewPersistentVolumeClaim,
+	neh.Add("Persistent Volume Claims", "persistent-volume-claims",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.PersistentVolumeClaim), objectStore))
-	neh.Add("Secrets", "secrets", icon.OverviewSecret,
+	neh.Add("Secrets", "secrets",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.Secret), objectStore))
-	neh.Add("Service Accounts", "service-accounts", icon.OverviewServiceAccount,
+	neh.Add("Service Accounts", "service-accounts",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.ServiceAccount), objectStore))
 
-	children, err := neh.Generate(prefix)
+	children, err := neh.Generate(prefix, namespace, "")
 	if err != nil {
 		return nil, false, err
 	}
@@ -93,12 +97,13 @@ func configAndStorageEntries(ctx context.Context, prefix, namespace string, obje
 func rbacEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, _ bool) ([]navigation.Navigation, bool, error) {
 	neh := navigation.EntriesHelper{}
 
-	neh.Add("Roles", "roles", icon.OverviewRole,
+	neh.Add("Overview", "", false)
+	neh.Add("Roles", "roles",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.Role), objectStore))
-	neh.Add("Role Bindings", "role-bindings", icon.OverviewRoleBinding,
+	neh.Add("Role Bindings", "role-bindings",
 		loading.IsObjectLoading(ctx, namespace, store.KeyFromGroupVersionKind(gvk.RoleBinding), objectStore))
 
-	children, err := neh.Generate(prefix)
+	children, err := neh.Generate(prefix, namespace, "")
 	if err != nil {
 		return nil, false, err
 	}
diff --git a/internal/modules/overview/overview.go b/internal/modules/overview/overview.go
index 52333e5f..578f22b0 100644
--- a/internal/modules/overview/overview.go
+++ b/internal/modules/overview/overview.go
@@ -195,6 +195,7 @@ func (co *Overview) Navigation(ctx context.Context, namespace, root string) ([]n
 	navigationEntries := octant.NavigationEntries{
 		Lookup: navPathLookup,
 		EntriesFuncs: map[string]octant.EntriesFunc{
+			"Overview":                     nil,
 			"Workloads":                    workloadEntries,
 			"Discovery and Load Balancing": discoAndLBEntries,
 			"Config and Storage":           configAndStorageEntries,
@@ -202,7 +203,17 @@ func (co *Overview) Navigation(ctx context.Context, namespace, root string) ([]n
 			"RBAC":                         rbacEntries,
 			"Events":                       nil,
 		},
+		IconMap: map[string]string{
+			"Overview":                     icon.Overview,
+			"Workloads":                    icon.Workloads,
+			"Discovery and Load Balancing": icon.DiscoveryAndLoadBalancing,
+			"Config and Storage":           icon.ConfigAndStorage,
+			"Custom Resources":             icon.CustomResources,
+			"RBAC":                         icon.RBAC,
+			"Events":                       icon.Events,
+		},
 		Order: []string{
+			"Overview",
 			"Workloads",
 			"Discovery and Load Balancing",
 			"Config and Storage",
@@ -216,14 +227,12 @@ func (co *Overview) Navigation(ctx context.Context, namespace, root string) ([]n
 
 	nf := octant.NewNavigationFactory(namespace, root, objectStore, navigationEntries)
 
-	entries, err := nf.Generate(ctx, "Overview", icon.Overview, "", false)
+	entries, err := nf.Generate(ctx, "", false)
 	if err != nil {
 		return nil, err
 	}
 
-	return []navigation.Navigation{
-		*entries,
-	}, nil
+	return entries, nil
 }
 
 // Generators allow modules to send events to the frontend.
diff --git a/internal/modules/workloads/module.go b/internal/modules/workloads/module.go
index db49277c..b59719f3 100644
--- a/internal/modules/workloads/module.go
+++ b/internal/modules/workloads/module.go
@@ -18,6 +18,7 @@ import (
 	"github.com/vmware-tanzu/octant/internal/generator"
 	"github.com/vmware-tanzu/octant/internal/module"
 	"github.com/vmware-tanzu/octant/internal/octant"
+	"github.com/vmware-tanzu/octant/pkg/icon"
 	"github.com/vmware-tanzu/octant/pkg/navigation"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
@@ -95,8 +96,9 @@ func (m *Module) Navigation(ctx context.Context, namespace, root string) ([]navi
 	rootPath := path.Join(m.ContentPath(), "namespace", namespace)
 
 	rootNav := navigation.Navigation{
-		Title: "Workloads",
-		Path:  rootPath,
+		Title:    "Applications",
+		Path:     rootPath,
+		IconName: icon.Applications,
 	}
 
 	return []navigation.Navigation{rootNav}, nil
diff --git a/internal/octant/factory.go b/internal/octant/factory.go
index 51a67be0..fccdcd4c 100644
--- a/internal/octant/factory.go
+++ b/internal/octant/factory.go
@@ -25,6 +25,7 @@ type EntriesFunc func(ctx context.Context, prefix, namespace string, objectStore
 type NavigationEntries struct {
 	Lookup       map[string]string
 	EntriesFuncs map[string]EntriesFunc
+	IconMap      map[string]string
 	Order        []string
 }
 
@@ -60,33 +61,30 @@ func (nf *NavigationFactory) Root() string {
 }
 
 // Generate returns navigation entries.
-func (nf *NavigationFactory) Generate(ctx context.Context, title string, iconName, iconSource string, wantsClusterScoped bool) (*navigation.Navigation, error) {
-	n := &navigation.Navigation{
-		Title:    title,
-		Path:     nf.rootPath,
-		Children: []navigation.Navigation{},
-	}
-
-	if iconName != "" {
-		n.IconName = iconName
-
-		if iconSource != "" {
-			n.IconSource = iconSource
-		}
-	}
+func (nf *NavigationFactory) Generate(ctx context.Context, module string, wantsClusterScoped bool) ([]navigation.Navigation, error) {
+	n := []navigation.Navigation{}
 
 	var mu sync.Mutex
 	var g errgroup.Group
 
-	for _, name := range nf.entries.Order {
+	for index, name := range nf.entries.Order {
 		g.Go(func() error {
-			children, err := nf.genNode(ctx, name, nf.entries.EntriesFuncs[name], wantsClusterScoped)
+			child, err := nf.genNode(ctx, name, nf.entries.EntriesFuncs[name], wantsClusterScoped)
 			if err != nil {
 				return errors.Wrapf(err, "generate entries for %s", name)
 			}
 
+			if iconName, ok := nf.entries.IconMap[name]; ok {
+				child.IconName = iconName
+			}
+
+			// Setting module creates a divider in navigation
+			if (module != "") && (index == 0) {
+				child.Module = module
+			}
+
 			mu.Lock()
-			n.Children = append(n.Children, *children)
+			n = append(n, *child)
 			mu.Unlock()
 
 			return nil
diff --git a/internal/printer/persistentvolume.go b/internal/printer/persistentvolume.go
index cbb19248..b49f5b7c 100644
--- a/internal/printer/persistentvolume.go
+++ b/internal/printer/persistentvolume.go
@@ -10,7 +10,7 @@ import (
 	"fmt"
 
 	"github.com/pkg/errors"
-	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/vmware-tanzu/octant/pkg/store"
@@ -18,7 +18,7 @@ import (
 )
 
 // PersistentVolumeListHandler is a printFunc that creates a component to display multiple Persistent Volumes
-func PersistentVolumeListHandler(ctx context.Context, list *corev1.PersistentVolumeList, options Options) (component.Component, error) {
+func PersistentVolumeListHandler(ctx context.Context, list *v1.PersistentVolumeList, options Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("nil list")
 	}
@@ -33,7 +33,7 @@ func PersistentVolumeListHandler(ctx context.Context, list *corev1.PersistentVol
 			return nil, err
 		}
 
-		storage := pv.Spec.Capacity[corev1.ResourceStorage]
+		storage := pv.Spec.Capacity[v1.ResourceStorage]
 		capacity := storage.String()
 
 		accessModes := getAccessModesAsString(pv.Spec.AccessModes)
@@ -60,7 +60,7 @@ func PersistentVolumeListHandler(ctx context.Context, list *corev1.PersistentVol
 }
 
 // PersistentVolumeHandler is a printFunc that creates a component to display a single Persistent Volume
-func PersistentVolumeHandler(ctx context.Context, pv *corev1.PersistentVolume, options Options) (component.Component, error) {
+func PersistentVolumeHandler(ctx context.Context, pv *v1.PersistentVolume, options Options) (component.Component, error) {
 	obj := NewObject(pv)
 	obj.EnableEvents()
 
@@ -81,13 +81,13 @@ func PersistentVolumeHandler(ctx context.Context, pv *corev1.PersistentVolume, o
 }
 
 type persistentVolumeHandler struct {
-	configFunc       func(*corev1.PersistentVolume, Options) (*component.Summary, error)
-	statusFunc       func(context.Context, *corev1.PersistentVolume, Options) (*component.Summary, error)
-	persistentVolume *corev1.PersistentVolume
+	configFunc       func(*v1.PersistentVolume, Options) (*component.Summary, error)
+	statusFunc       func(context.Context, *v1.PersistentVolume, Options) (*component.Summary, error)
+	persistentVolume *v1.PersistentVolume
 	object           *Object
 }
 
-func newPersistentVolumeHandler(pv *corev1.PersistentVolume, object *Object) (*persistentVolumeHandler, error) {
+func newPersistentVolumeHandler(pv *v1.PersistentVolume, object *Object) (*persistentVolumeHandler, error) {
 	if pv == nil {
 		return nil, errors.New("cannot print a nil persistentVolume")
 	}
@@ -123,22 +123,22 @@ func (pvh *persistentVolumeHandler) Status(ctx context.Context, options Options)
 	return nil
 }
 
-func defaultPersistentVolumeConfig(pv *corev1.PersistentVolume, options Options) (*component.Summary, error) {
+func defaultPersistentVolumeConfig(pv *v1.PersistentVolume, options Options) (*component.Summary, error) {
 	return NewPersistentVolumeConfiguration(pv).Create(options)
 }
 
-func defaultPersistentVolumeStatus(ctx context.Context, pv *corev1.PersistentVolume, options Options) (*component.Summary, error) {
+func defaultPersistentVolumeStatus(ctx context.Context, pv *v1.PersistentVolume, options Options) (*component.Summary, error) {
 	return NewPersistentVolumeStatus(pv).Create(ctx, options)
 }
 
 // PersistentVolumeConfiguration is used to create the Persistent Volume's configuration component
 // when displaying a single Persistent Volume
 type PersistentVolumeConfiguration struct {
-	persistentVolume *corev1.PersistentVolume
+	persistentVolume *v1.PersistentVolume
 }
 
 // NewPersistentVolumeConfiguration creates a new PersistentVolumeConfiguration using the supplied Persistent Volume
-func NewPersistentVolumeConfiguration(pv *corev1.PersistentVolume) *PersistentVolumeConfiguration {
+func NewPersistentVolumeConfiguration(pv *v1.PersistentVolume) *PersistentVolumeConfiguration {
 	return &PersistentVolumeConfiguration{
 		persistentVolume: pv,
 	}
@@ -152,7 +152,7 @@ func (pvc *PersistentVolumeConfiguration) Create(options Options) (*component.Su
 	pv := pvc.persistentVolume
 
 	accessModes := getAccessModesAsString(pv.Spec.AccessModes)
-	storage := pv.Spec.Capacity[corev1.ResourceStorage]
+	storage := pv.Spec.Capacity[v1.ResourceStorage]
 	capacity := storage.String()
 
 	var sections component.SummarySections
@@ -160,11 +160,7 @@ func (pvc *PersistentVolumeConfiguration) Create(options Options) (*component.Su
 	sections.AddText("Storage Class", pv.Spec.StorageClassName)
 	sections.AddText("Access Modes", accessModes)
 	sections.AddText("Capacity", capacity)
-
-	sections, err := printPersistentVolumeSource(pv, sections)
-	if err != nil {
-		return nil, err
-	}
+	sections.AddText("Host Path", pv.Spec.HostPath.Path)
 
 	summary := component.NewSummary("Configuration", sections...)
 	return summary, nil
@@ -173,11 +169,11 @@ func (pvc *PersistentVolumeConfiguration) Create(options Options) (*component.Su
 // PersistentVolumeStatus is used to create the Persistent Volume's status component
 // when displaying a single Persistent Volume
 type PersistentVolumeStatus struct {
-	persistentVolume *corev1.PersistentVolume
+	persistentVolume *v1.PersistentVolume
 }
 
 // NewPersistentVolumeStatus creates a new PersistentVolumeStatus using the supplied Persistent Volume
-func NewPersistentVolumeStatus(pv *corev1.PersistentVolume) *PersistentVolumeStatus {
+func NewPersistentVolumeStatus(pv *v1.PersistentVolume) *PersistentVolumeStatus {
 	return &PersistentVolumeStatus{
 		persistentVolume: pv,
 	}
@@ -208,9 +204,9 @@ func (pvs *PersistentVolumeStatus) Create(ctx context.Context, options Options)
 	return summary, nil
 }
 
-func getBoundPersistentVolumeClaim(ctx context.Context, pv *corev1.PersistentVolume, options Options) (*corev1.PersistentVolumeClaim, error) {
+func getBoundPersistentVolumeClaim(ctx context.Context, pv *v1.PersistentVolume, options Options) (*v1.PersistentVolumeClaim, error) {
 	objectStore := options.DashConfig.ObjectStore()
-	pvc := &corev1.PersistentVolumeClaim{}
+	pvc := &v1.PersistentVolumeClaim{}
 
 	cr := pv.Spec.ClaimRef
 	key := store.Key{
@@ -237,7 +233,7 @@ func getBoundPersistentVolumeClaim(ctx context.Context, pv *corev1.PersistentVol
 	return pvc, nil
 }
 
-func createBoundPersistentVolumeClaimLink(ctx context.Context, pv *corev1.PersistentVolume, options Options) (*component.Link, error) {
+func createBoundPersistentVolumeClaimLink(ctx context.Context, pv *v1.PersistentVolume, options Options) (*component.Link, error) {
 	pvc, err := getBoundPersistentVolumeClaim(ctx, pv, options)
 	if err != nil {
 		return nil, err
@@ -252,55 +248,3 @@ func createBoundPersistentVolumeClaimLink(ctx context.Context, pv *corev1.Persis
 
 	return claimLink, nil
 }
-
-func printPersistentVolumeSource(pv *corev1.PersistentVolume, section component.SummarySections) (component.SummarySections, error) {
-	switch {
-	case pv.Spec.PersistentVolumeSource.GCEPersistentDisk != nil:
-		section.AddText("GCE Persistent Disk", describeVolumeSource(pv.Spec.PersistentVolumeSource.GCEPersistentDisk))
-	case pv.Spec.PersistentVolumeSource.AWSElasticBlockStore != nil:
-		section.AddText("AWS Elastic Block Store", describeVolumeSource(pv.Spec.PersistentVolumeSource.AWSElasticBlockStore))
-	case pv.Spec.PersistentVolumeSource.HostPath != nil:
-		section.AddText("Host Path", describeVolumeSource(pv.Spec.PersistentVolumeSource.HostPath))
-	case pv.Spec.PersistentVolumeSource.Glusterfs != nil:
-		section.AddText("GlusterFS", describeVolumeSource(pv.Spec.PersistentVolumeSource.Glusterfs))
-	case pv.Spec.PersistentVolumeSource.NFS != nil:
-		section.AddText("NFS", describeVolumeSource(pv.Spec.PersistentVolumeSource.NFS))
-	case pv.Spec.PersistentVolumeSource.RBD != nil:
-		section.AddText("RBD", describeVolumeSource(pv.Spec.PersistentVolumeSource.RBD))
-	case pv.Spec.PersistentVolumeSource.ISCSI != nil:
-		section.AddText("ISCI", describeVolumeSource(pv.Spec.PersistentVolumeSource.ISCSI))
-	case pv.Spec.PersistentVolumeSource.Cinder != nil:
-		section.AddText("Cinder", describeVolumeSource(pv.Spec.PersistentVolumeSource.Cinder))
-	case pv.Spec.PersistentVolumeSource.CephFS != nil:
-		section.AddText("CephFS", describeVolumeSource(pv.Spec.PersistentVolumeSource.CephFS))
-	case pv.Spec.PersistentVolumeSource.FC != nil:
-		section.AddText("FC", describeVolumeSource(pv.Spec.PersistentVolumeSource.FC))
-	case pv.Spec.PersistentVolumeSource.Flocker != nil:
-		section.AddText("Flocker", describeVolumeSource(pv.Spec.PersistentVolumeSource.Flocker))
-	case pv.Spec.PersistentVolumeSource.FlexVolume != nil:
-		section.AddText("Flex Volume", describeVolumeSource(pv.Spec.PersistentVolumeSource.FlexVolume))
-	case pv.Spec.PersistentVolumeSource.AzureFile != nil:
-		section.AddText("Azure File", describeVolumeSource(pv.Spec.PersistentVolumeSource.AzureFile))
-	case pv.Spec.PersistentVolumeSource.VsphereVolume != nil:
-		section.AddText("Vsphere Volume", describeVolumeSource(pv.Spec.PersistentVolumeSource.VsphereVolume))
-	case pv.Spec.PersistentVolumeSource.Quobyte != nil:
-		section.AddText("Quobyte", describeVolumeSource(pv.Spec.PersistentVolumeSource.Quobyte))
-	case pv.Spec.PersistentVolumeSource.AzureDisk != nil:
-		section.AddText("Azure Disk", describeVolumeSource(pv.Spec.PersistentVolumeSource.AzureDisk))
-	case pv.Spec.PersistentVolumeSource.PhotonPersistentDisk != nil:
-		section.AddText("Photon Persistent Disk", describeVolumeSource(pv.Spec.PersistentVolumeSource.PhotonPersistentDisk))
-	case pv.Spec.PersistentVolumeSource.PortworxVolume != nil:
-		section.AddText("Portworx Volume", describeVolumeSource(pv.Spec.PersistentVolumeSource.PortworxVolume))
-	case pv.Spec.PersistentVolumeSource.ScaleIO != nil:
-		section.AddText("ScaleIO", describeVolumeSource(pv.Spec.PersistentVolumeSource.ScaleIO))
-	case pv.Spec.PersistentVolumeSource.Local != nil:
-		section.AddText("Local", describeVolumeSource(pv.Spec.PersistentVolumeSource.Local))
-	case pv.Spec.PersistentVolumeSource.StorageOS != nil:
-		section.AddText("StorageOS", describeVolumeSource(pv.Spec.PersistentVolumeSource.StorageOS))
-	case pv.Spec.PersistentVolumeSource.CSI != nil:
-		section.AddText("CSI", describeVolumeSource(pv.Spec.PersistentVolumeSource.CSI))
-	default:
-		section.AddText("Persistent Volume Source", "Unknown")
-	}
-	return section, nil
-}
diff --git a/pkg/dash/dash.go b/pkg/dash/dash.go
index 5b46fe92..f3ca8221 100644
--- a/pkg/dash/dash.go
+++ b/pkg/dash/dash.go
@@ -11,10 +11,15 @@ import (
 	"fmt"
 	"net"
 	"net/http"
+	"net/http/httputil"
+	"net/url"
 	"os"
+	"strings"
 	"time"
 
 	"contrib.go.opencensus.io/exporter/jaeger"
+	"github.com/gorilla/handlers"
+	"github.com/gorilla/mux"
 	"github.com/skratchdot/open-golang/open"
 	"github.com/spf13/viper"
 	"go.opencensus.io/trace"
@@ -37,7 +42,6 @@ import (
 	"github.com/vmware-tanzu/octant/internal/terminal"
 	"github.com/vmware-tanzu/octant/pkg/action"
 	"github.com/vmware-tanzu/octant/pkg/log"
-	"github.com/vmware-tanzu/octant/pkg/octant"
 	"github.com/vmware-tanzu/octant/pkg/plugin"
 	pluginAPI "github.com/vmware-tanzu/octant/pkg/plugin/api"
 	"github.com/vmware-tanzu/octant/pkg/store"
@@ -363,11 +367,7 @@ func newDash(listener net.Listener, namespace, uiURL string, apiHandler api.Serv
 }
 
 func (d *dash) Run(ctx context.Context) error {
-	hf := octant.NewHandlerFactory(
-		octant.BackendHandler(d.apiHandler.Handler),
-		octant.FrontendURL(viper.GetString("proxy-frontend")))
-
-	handler, err := hf.Handler(ctx)
+	handler, err := d.handler(ctx)
 	if err != nil {
 		return err
 	}
@@ -398,6 +398,74 @@ func (d *dash) Run(ctx context.Context) error {
 	return server.Shutdown(shutdownCtx)
 }
 
+// handler configures primary http routes
+func (d *dash) handler(ctx context.Context) (http.Handler, error) {
+	var frontendHandler http.Handler
+	frontendPath := viper.GetString("proxy-frontend")
+	if frontendPath == "" {
+		d.logger.Infof("Using embedded Octant frontend")
+		// use embedded assets
+		handler, err := d.uiHandler()
+		if err != nil {
+			return nil, err
+		}
+		frontendHandler = handler
+	} else {
+		d.logger.With("proxy-path", frontendPath).Infof("Creating reverse proxy to Octant frontend")
+		// use reverse proxy
+		proxyURL, err := url.Parse(frontendPath)
+		if err != nil {
+			return nil, err
+		}
+		frontendHandler = httputil.NewSingleHostReverseProxy(proxyURL)
+	}
+
+	router := mux.NewRouter()
+	apiHandler, err := d.apiHandler.Handler(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	router.PathPrefix(api.PathPrefix).Handler(apiHandler)
+
+	router.PathPrefix("/").Handler(frontendHandler)
+
+	allowedOrigins := handlers.AllowedOrigins([]string{"*"})
+	allowedHeaders := handlers.AllowedHeaders([]string{"Accept", "Accept-Language", "Content-Language", "Origin", "Content-Type"})
+	allowedMethods := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"})
+
+	return handlers.CORS(allowedOrigins, allowedHeaders, allowedMethods)(router), nil
+}
+
+func (d *dash) uiHandler() (http.Handler, error) {
+	if d.uiURL == "" {
+		return d.defaultHandler()
+	}
+
+	return d.uiProxy()
+}
+
+func (d *dash) uiProxy() (*httputil.ReverseProxy, error) {
+	uiURL := d.uiURL
+
+	if !strings.HasPrefix(uiURL, "http") && !strings.HasPrefix(uiURL, "https") {
+		uiURL = fmt.Sprintf("http://%s", uiURL)
+	}
+	u, err := url.Parse(uiURL)
+	if err != nil {
+		return nil, err
+	}
+
+	if u.Scheme == "" {
+		u.Scheme = "http"
+	}
+
+	d.logger.Infof("Proxying dashboard UI to %s", u.String())
+
+	proxy := httputil.NewSingleHostReverseProxy(u)
+	return proxy, nil
+}
+
 func enableOpenCensus() error {
 	agentEndpointURI := "localhost:6831"
 
diff --git a/pkg/icon/icon.go b/pkg/icon/icon.go
index 3b43ebfe..09ca5067 100644
--- a/pkg/icon/icon.go
+++ b/pkg/icon/icon.go
@@ -15,6 +15,20 @@ import (
 //go:generate rice embed-go
 
 const (
+	// Names of Clarity icons
+	Applications              = "application"
+	Workloads                 = "applications"
+	Overview                  = "dashboard"
+	DiscoveryAndLoadBalancing = "network-globe"
+	ConfigAndStorage          = "storage"
+	RBAC                      = "assign-user"
+	Events                    = "event"
+
+	Namespaces      = "namespace"
+	CustomResources = "file-group"
+	Nodes           = "nodes"
+	PortForwards    = "router"
+
 	ClusterOverview                   = "objects"
 	ClusterOverviewClusterRole        = "c-role"
 	ClusterOverviewClusterRoleBinding = "crb"
@@ -27,7 +41,6 @@ const (
 
 	CustomResourceDefinition = "crd"
 
-	Overview                        = "objects"
 	OverviewConfigMap               = "cm"
 	OverviewCronJob                 = "cronjob"
 	OverviewDaemonSet               = "ds"
diff --git a/pkg/navigation/navigation.go b/pkg/navigation/navigation.go
index 25da344c..1183dea6 100644
--- a/pkg/navigation/navigation.go
+++ b/pkg/navigation/navigation.go
@@ -55,6 +55,7 @@ func SetLoading(isLoading bool) Option {
 
 // Navigation is a set of navigation entries.
 type Navigation struct {
+	Module     string       `json:"module,omitempty"`
 	Title      string       `json:"title,omitempty"`
 	Path       string       `json:"path,omitempty"`
 	Children   []Navigation `json:"children,omitempty"`
@@ -78,7 +79,13 @@ func New(title, navigationPath string, options ...Option) (*Navigation, error) {
 
 // CRDEntries generates navigation entries for CRDs.
 func CRDEntries(ctx context.Context, prefix, namespace string, objectStore store.Store, wantsClusterScoped bool) ([]Navigation, bool, error) {
-	var list []Navigation
+	var list = []Navigation{
+		{
+			Title:   "Overview",
+			Path:    prefix,
+			Loading: false,
+		},
+	}
 
 	loading := false
 
@@ -218,14 +225,14 @@ type EntriesHelper struct {
 }
 
 // Add adds an entry.
-func (neh *EntriesHelper) Add(title, suffix, iconName string, isLoading bool) {
+func (neh *EntriesHelper) Add(title, suffix string, isLoading bool) {
 	neh.navConfigs = append(neh.navConfigs, navConfig{
-		title: title, suffix: suffix, iconName: iconName, isLoading: isLoading,
+		title: title, suffix: suffix, isLoading: isLoading,
 	})
 }
 
 // Generate generates navigation entries.
-func (neh *EntriesHelper) Generate(prefix string) ([]Navigation, error) {
+func (neh *EntriesHelper) Generate(prefix, namespace, name string) ([]Navigation, error) {
 	var navigations []Navigation
 
 	for _, nc := range neh.navConfigs {
diff --git a/pkg/octant/handler.go b/pkg/octant/handler.go
deleted file mode 100644
index 505275c6..00000000
--- a/pkg/octant/handler.go
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- *  Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- *  SPDX-License-Identifier: Apache-2.0
- *
- */
-
-package octant
-
-import (
-	"context"
-	"net/http"
-	"net/http/httputil"
-	"net/url"
-
-	"github.com/gorilla/handlers"
-	"github.com/gorilla/mux"
-
-	"github.com/vmware-tanzu/octant/internal/api"
-	"github.com/vmware-tanzu/octant/internal/log"
-)
-
-// HandlerFactory is a factory that generate's Octant's HTTP handler. Octant has
-// a frontend and backend handler and they will both be populated in the
-// generated handler.
-type HandlerFactory struct {
-	frontendHandler func(ctx context.Context) (http.Handler, error)
-	backendHandler  func(ctx context.Context) (http.Handler, error)
-}
-
-// NewHandlerFactory creates an instance of HandlerFactory.
-func NewHandlerFactory(optionList ...Option) *HandlerFactory {
-	opts := buildOptions(optionList...)
-
-	hf := HandlerFactory{
-		frontendHandler: opts.frontendHandler,
-		backendHandler:  opts.backendHandler,
-	}
-
-	return &hf
-}
-
-// Handler returns an HTTP handler.
-func (hf *HandlerFactory) Handler(ctx context.Context) (http.Handler, error) {
-	router := mux.NewRouter()
-
-	backendHandler, err := hf.backendHandler(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	frontendHandler, err := hf.frontendHandler(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	router.PathPrefix(api.PathPrefix).Handler(backendHandler)
-
-	router.PathPrefix("/").Handler(frontendHandler)
-
-	allowedOrigins := handlers.AllowedOrigins([]string{"*"})
-	allowedHeaders := handlers.AllowedHeaders([]string{"Accept", "Accept-Language", "Content-Language", "Origin", "Content-Type"})
-	allowedMethods := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"})
-
-	return handlers.CORS(allowedOrigins, allowedHeaders, allowedMethods)(router), nil
-}
-
-// ProxiedFrontendHandler creates an HTTP handler that proxies to a target URL.
-type ProxiedFrontendHandler struct {
-	handler *httputil.ReverseProxy
-}
-
-var _ http.Handler = &ProxiedFrontendHandler{}
-
-// NewProxiedFrontend creates an instance of ProxiedFrontendHandler. It will return an
-// error if the proxy can not be created.
-func NewProxiedFrontend(ctx context.Context, targetURL string) (*ProxiedFrontendHandler, error) {
-	logger := log.From(ctx)
-	logger.With("proxy-url", targetURL).
-		Infof("Creating reverse proxy for Octant's frontend")
-
-	handler, err := createProxyTarget(targetURL)
-	if err != nil {
-		return nil, err
-	}
-
-	pf := ProxiedFrontendHandler{
-		handler: handler,
-	}
-
-	return &pf, nil
-}
-
-// ServeHTTP allows ProxiedFrontendHandler to satisfy the http.Handler interface.
-func (p ProxiedFrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	p.handler.ServeHTTP(w, r)
-}
-
-func createProxyTarget(targetURL string) (*httputil.ReverseProxy, error) {
-	target, err := url.Parse(targetURL)
-	if err != nil {
-		return nil, err
-	}
-
-	if target.Scheme == "" {
-		target.Scheme = "http"
-	}
-
-	handler := httputil.NewSingleHostReverseProxy(target)
-	return handler, nil
-}
diff --git a/pkg/octant/options.go b/pkg/octant/options.go
deleted file mode 100644
index d0bbd4e7..00000000
--- a/pkg/octant/options.go
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- *  Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- *  SPDX-License-Identifier: Apache-2.0
- *
- */
-
-package octant
-
-import (
-	"context"
-	"fmt"
-	"net/http"
-
-	"github.com/vmware-tanzu/octant/web"
-)
-
-// options is an internal set of options that can be used to configure Octant. These are
-// consolidated options so there is not a need to have a separate set of options
-// for multiple types. options is not exported as these options should be accessible from
-// outside of this package.
-type options struct {
-	// frontendHandler is a function that creates a frontend handler.
-	frontendHandler func(ctx context.Context) (http.Handler, error)
-	// backendHandler is a function that creates a backend handler.
-	backendHandler func(ctx context.Context) (http.Handler, error)
-}
-
-// buildOptions builds an options struct from a list of functional options.
-func buildOptions(list ...Option) options {
-	opts := options{
-		frontendHandler: defaultFrontendHandler,
-		backendHandler: func(ctx context.Context) (handler http.Handler, err error) {
-			return nil, fmt.Errorf("backend handler is not configured")
-		},
-	}
-
-	for _, o := range list {
-		o(&opts)
-	}
-
-	return opts
-}
-
-// Option is a functional option for configuring Octant.
-type Option func(o *options)
-
-// FrontendURL configures Octant to use a proxy for rendering its frontend.
-func FrontendURL(proxyURL string) Option {
-	return func(o *options) {
-		o.frontendHandler = func(ctx context.Context) (handler http.Handler, err error) {
-			if proxyURL == "" {
-				o.frontendHandler = defaultFrontendHandler
-				return
-			}
-
-			pfh, err := NewProxiedFrontend(ctx, proxyURL)
-			if err != nil {
-				return nil, err
-			}
-
-			return pfh, nil
-		}
-	}
-}
-
-// BackendHandler sets the handler for Octant's backend.
-func BackendHandler(fn func(ctx context.Context) (http.Handler, error)) Option {
-	return func(o *options) {
-		o.backendHandler = fn
-	}
-}
-
-// defaultFrontendHandler is the default factory for creating a frontend handler.
-// TODO: this namespace should not know about the web namespace.
-func defaultFrontendHandler(ctx context.Context) (http.Handler, error) {
-	return web.Handler()
-}
diff --git a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.ts b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.ts
index 7a40b35f..e339b233 100644
--- a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.ts
+++ b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.ts
@@ -59,8 +59,9 @@ export class DatagridComponent implements OnChanges {
 
         const current = changes.view.currentValue as TableView;
         this.columns = current.config.columns.map(column => column.name);
-        this.rowsWithMetadata = this.getRowsWithMetadata(current.config.rows);
-
+        if (current.config.rows) {
+          this.rowsWithMetadata = this.getRowsWithMetadata(current.config.rows);
+        }
         this.placeholder = current.config.emptyContent;
         this.lastUpdated = new Date();
         this.loading = current.config.loading;
diff --git a/web/src/app/modules/shared/components/smart/context-selector/context-selector.component.scss b/web/src/app/modules/shared/components/smart/context-selector/context-selector.component.scss
index f2b50250..b8a9fab2 100644
--- a/web/src/app/modules/shared/components/smart/context-selector/context-selector.component.scss
+++ b/web/src/app/modules/shared/components/smart/context-selector/context-selector.component.scss
@@ -9,5 +9,13 @@
 }
 
 .dropdown {
-  margin: 0 1rem;
+  margin: 0 0.8rem;
+}
+
+.dropdown .dropdown-toggle clr-icon {
+  margin-right: 0.2rem;
+}
+
+.dropdown .dropdown-toggle:not(.btn) {
+  padding: 0 1.0rem 0 0;
 }
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 90dfdfbc..44030155 100644
--- a/web/src/app/modules/shared/services/navigation/navigation.service.ts
+++ b/web/src/app/modules/shared/services/navigation/navigation.service.ts
@@ -8,6 +8,8 @@ import { WebsocketService } from '../websocket/websocket.service';
 import { BehaviorSubject } from 'rxjs';
 import { Navigation } from '../../../sugarloaf/models/navigation';
 import { ContentService } from '../content/content.service';
+import { NavigationEnd, Router, RouterEvent } from '@angular/router';
+import { filter } from 'rxjs/operators';
 
 const emptyNavigation: Navigation = {
   sections: [],
@@ -19,10 +21,19 @@ const emptyNavigation: Navigation = {
 })
 export class NavigationService {
   current = new BehaviorSubject<Navigation>(emptyNavigation);
+  public lastSelection: BehaviorSubject<number> = new BehaviorSubject<number>(
+    -1
+  );
+  public expandedState: BehaviorSubject<any> = new BehaviorSubject<any>({});
+  public collapsed: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
+    true
+  );
+  activeUrl = new BehaviorSubject<string>('');
 
   constructor(
     private websocketService: WebsocketService,
-    private contentService: ContentService
+    private contentService: ContentService,
+    private router: Router
   ) {
     websocketService.registerHandler('navigation', data => {
       const update = data as Navigation;
@@ -30,5 +41,11 @@ export class NavigationService {
 
       contentService.defaultPath.next(update.defaultPath);
     });
+
+    router.events
+      .pipe(filter(e => e instanceof NavigationEnd))
+      .subscribe((event: RouterEvent) => {
+        this.activeUrl.next(event.url);
+      });
   }
 }
diff --git a/web/src/app/modules/sugarloaf/components/smart/container/container.component.html b/web/src/app/modules/sugarloaf/components/smart/container/container.component.html
index c411d2f1..88acf17f 100644
--- a/web/src/app/modules/sugarloaf/components/smart/container/container.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/container/container.component.html
@@ -15,15 +15,14 @@
       </div>
     </div>
     <div class="header-actions">
-      <app-theme-switch-button></app-theme-switch-button>
-      <app-context-selector></app-context-selector>
+      <div class="namespace-switcher header-centered">
+        <app-namespace></app-namespace>
+      </div>
+      <app-context-selector class="header-centered"></app-context-selector>
     </div>
   </header>
   <div class="content-container">
     <div class="navigation">
-      <div class="namespace-switcher">
-        <app-namespace></app-namespace>
-      </div>
       <app-navigation></app-navigation>
     </div>
     <div class="content-area">
diff --git a/web/src/app/modules/sugarloaf/components/smart/container/container.component.scss b/web/src/app/modules/sugarloaf/components/smart/container/container.component.scss
index a8b72da8..8bf8f323 100644
--- a/web/src/app/modules/sugarloaf/components/smart/container/container.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/container/container.component.scss
@@ -2,50 +2,52 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-$navigation-width: 13rem;
-
 .main-container {
   // Navigation's color variables for Light Theme.
   :host-context(body) {
     --navigation-border-color: rgb(215, 215, 215);
-    --navigation-background-color: #0079ad;
+    --navigation-background-color: #0079AD;
   }
 
   // Navigation's color variables for Dark Theme.
   :host-context(body.dark) {
-    --navigation-border-color: #0e161b;
-    --navigation-background-color: #0e161b;
+    --navigation-border-color: #304250;
+    --navigation-background-color: #304250;
+  }
+
+  .header-centered {
+    margin: auto 0;
   }
 
   .header {
     background: var(--navigation-background-color);
 
-    .branding {
-      min-width: $navigation-width;
-    }
-
     .header-nav {
       padding-left: 1rem;
 
+      .namespace-switcher,
       .input-filter {
         margin-right: 10px;
+      }
+
+      .namespace-switcher {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        margin-right: 20px;
+      }
+
+      .input-filter {
         z-index: 999; // clarity's data-grid beats this
         width: 300px;
       }
     }
-
-    .header-actions {
-      display: flex;
-      align-items: center;
-    }
   }
 
   .content-container {
     .navigation {
       border-right: 1px solid var(--navigation-border-color);
-      overflow-y: auto;
-      min-width: 2rem;
-      width: $navigation-width;
     }
 
     .content-area {
diff --git a/web/src/app/modules/sugarloaf/components/smart/content/content.component.html b/web/src/app/modules/sugarloaf/components/smart/content/content.component.html
index c0d427e6..bc4579bf 100644
--- a/web/src/app/modules/sugarloaf/components/smart/content/content.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/content/content.component.html
@@ -1,4 +1,4 @@
-<div class="content-component">
+<div clrFocusOnViewInit class="content-component">
   <ng-container *ngIf="hasReceivedContent">
     <ng-container
       *ngIf="hasTabs; then withTabs; else withoutTabs"
diff --git a/web/src/app/modules/sugarloaf/components/smart/content/content.component.ts b/web/src/app/modules/sugarloaf/components/smart/content/content.component.ts
index f3dbaa45..d8e912a4 100644
--- a/web/src/app/modules/sugarloaf/components/smart/content/content.component.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/content/content.component.ts
@@ -31,7 +31,6 @@ import { Subscription } from 'rxjs';
   changeDetection: ChangeDetectionStrategy.Default,
 })
 export class ContentComponent implements OnInit, OnDestroy {
-  @ViewChild('scrollTarget', { static: true }) scrollTarget: ElementRef;
   hasTabs = false;
   hasReceivedContent = false;
   title: string = null;
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 71f13a26..129b15c1 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,10 +1,27 @@
 <div class="app-namespace-selector">
-  <span class="title">Namespace</span>
-  <ng-select
-    class="namespace-dropdown"
-    [items]="namespaces"
-    (change)="selectNamespace($event)"
-    [(ngModel)]="currentNamespace"
-    [clearable]="false"
-  ></ng-select>
+  <ng-template [ngIf]="showDropdown()" [ngIfElse]="hideDropdown">
+    <clr-dropdown [clrCloseMenuOnItemClick]="true">
+      <button type="button" clrDropdownTrigger>
+        <clr-icon shape="namespace"></clr-icon>
+      {{ currentNamespace }}
+        <clr-icon shape="caret down"></clr-icon>
+      </button>
+      <clr-dropdown-menu *clrIfOpen [clrPosition]="'bottom-right'">
+        <label class="dropdown-header">Namespaces</label>
+        <ng-container *ngFor="let namespace of namespaces;trackBy: trackByIdentity">
+          <button
+            type="button"
+            [ngClass]="namespaceClass(namespace)"
+            clrDropdownItem
+            (click)="selectNamespace(namespace)">
+          {{ namespace }}
+          </button>
+        </ng-container>
+      </clr-dropdown-menu>
+    </clr-dropdown>
+  </ng-template>
+  <ng-template #hideDropdown>
+    <clr-icon shape="namespace"></clr-icon>
+    [All Namespaces]
+  </ng-template>
 </div>
diff --git a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.scss b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.scss
index 6b7ef8b4..b8a9fab2 100644
--- a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.scss
@@ -2,92 +2,20 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-.app-namespace-selector {
-  // Dropdown's color variables for Light Theme.
-  :host-context(body) {
-    --dropdownLink-color: #575757;
-    --dropdownLinkActive-color: #49afd9;
-    --dropdownPanel-border-color: #ccc;
-    --dropdownOptionSelected-bg-color: #dae4ea;
-    --dropdownOption-bg-color: white;
-    --dropdownOption-font-color: black;
-  }
-
-  // Dropdown's color variables for Dark Theme.
-  :host-context(body.dark) {
-    --dropdownLink-color: #49afd9;
-    --dropdownLinkActive-color: #49afd9;
-    --dropdownPanel-border-color: black;
-    --dropdownOptionSelected-bg-color: #324f60;
-    --dropdownOption-bg-color: #23353d;
-    --dropdownOption-font-color: #adbbc4;
-  }
-
-  margin: 0.5rem 1rem 1rem 1rem;
-
-  .title {
-    display: block;
-    margin-bottom: 0.25rem;
-    font-weight: 600;
-  }
-
-  .namespace-dropdown {
-    ::ng-deep {
-      .ng-select-container {
-        border-color: var(--dropdownLink-color);
-        background: transparent;
-      }
-
-      .ng-value,
-      .ng-input > input {
-        color: var(--dropdownLink-color);
-      }
-
-      .ng-arrow {
-        border-color: var(--dropdownLink-color) transparent transparent;
-      }
-
-      .ng-input {
-        padding-left: 5px;
-      }
-    }
-
-    &.ng-select-opened ::ng-deep {
-      .ng-select-container {
-        border-color: var(--dropdownLinkActive-color);
-      }
-
-      .ng-value {
-        color: var(--dropdownLinkActive-color);
-      }
-
-      .ng-arrow {
-        border-color: transparent transparent var(--dropdownLinkActive-color);
-      }
-    }
-
-    ::ng-deep .ng-dropdown-panel {
-      box-shadow: 0 1px 0.125rem rgba(0, 0, 0, 0.5);
-      border-color: var(--dropdownPanel-border-color);
-      background-color: var(--dropdownOption-bg-color);
-
-      .ng-dropdown-panel-items .ng-option {
-        background-color: var(--dropdownOption-bg-color);
-        color: var(--dropdownOption-font-color);
+.dropdown > .dropdown-toggle {
+  line-height: 2.5rem;
+  color: white;
+  outline: none;
+}
 
-        &:hover,
-        &.ng-option-selected {
-          background-color: var(--dropdownOptionSelected-bg-color);
-        }
-      }
+.dropdown {
+  margin: 0 0.8rem;
+}
 
-      &.ng-select-bottom {
-        border-top-color: var(--dropdownLink-color);
-      }
+.dropdown .dropdown-toggle clr-icon {
+  margin-right: 0.2rem;
+}
 
-      &.ng-select-top {
-        border-bottom-color: var(--dropdownLink-color);
-      }
-    }
-  }
+.dropdown .dropdown-toggle:not(.btn) {
+  padding: 0 1.0rem 0 0;
 }
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 cbbcc677..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
@@ -6,6 +6,7 @@ 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';
+import { NavigationService } from '../../../../shared/services/navigation/navigation.service';
 
 @Component({
   selector: 'app-namespace',
@@ -16,10 +17,19 @@ export class NamespaceComponent implements OnInit, OnDestroy {
   namespaces: string[];
   currentNamespace = '';
   trackByIdentity = trackByIdentity;
+  navigation = {
+    sections: [],
+    defaultPath: '',
+  };
+  lastSelection: number;
+  activeUrl: string;
 
   private namespaceSubscription: Subscription;
 
-  constructor(private namespaceService: NamespaceService) {}
+  constructor(
+    private namespaceService: NamespaceService,
+    private navigationService: NavigationService
+  ) {}
 
   ngOnInit() {
     this.namespaceSubscription = this.namespaceService.activeNamespace.subscribe(
@@ -33,13 +43,62 @@ export class NamespaceComponent implements OnInit, OnDestroy {
         this.namespaces = namespaces;
       }
     );
+
+    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 {
     this.namespaceSubscription.unsubscribe();
   }
 
+  namespaceClass(namespace: string) {
+    const active = this.currentNamespace === namespace ? ['active'] : [];
+    return ['context-button', ...active];
+  }
+
   selectNamespace(namespace: string) {
     this.namespaceService.setNamespace(namespace);
   }
+
+  showDropdown() {
+    if (this.lastSelection && this.navigation.sections[this.lastSelection]) {
+      const url = this.navigation.sections[this.lastSelection].path;
+      return !this.isClusterSpecific(url);
+    }
+    return true;
+  }
+
+  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();
+    if (ns.length > 0) {
+      this.selectNamespace(ns);
+      return false;
+    }
+
+    if (url.includes('cluster-overview')) {
+      return !this.activeUrl.endsWith(this.currentNamespace);
+    }
+
+    return false;
+  }
 }
diff --git a/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.html b/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.html
index 082b5476..772cfab8 100644
--- a/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.html
@@ -1,58 +1,110 @@
-<clr-tree>
-  <clr-tree-node
-    *ngFor="let section of navigation.sections; trackBy: identifyNavigationItem"
-    [clrExpanded]="true"
-    [clrLoading]="section.isLoading"
-    class="section"
-  >
-    <a
-      [routerLinkActiveOptions]="{ exact: true }"
-      [routerLink]="section.path | formatpath"
-      class="clr-treenode-link section"
-      routerLinkActive="active"
-    >
-      <span>
-        <clr-icon [attr.shape]="section.iconName | default: 'home'"></clr-icon>
-        {{ section.title }}
-      </span>
-    </a>
+<clr-vertical-nav [clrVerticalNavCollapsible]="true" [(clrVerticalNavCollapsed)]="collapsed" (clrVerticalNavCollapsedChange)="updateNavCollapsed($event)">
+  <!-- Navigation items -->
+  <div [ngClass]= "{'navigation-items-collapsed' : collapsed, 'navigation-items' : !collapsed}">
+    <ng-container *ngFor="let section of navigation.sections; let i = index; trackBy: identifyNavigationItem">
+      <!-- Collapsed Navigation -->
+      <ng-container *ngIf="collapsed; else expanded">
+        <ng-template [ngIf]="section.module">
+          <div class="nav-divider"></div>
+        </ng-template>
 
-    <clr-tree-node
-      *ngFor="let category of section.children; trackBy: identifyNavigationItem"
-      [clrExpanded]="true"
-      [clrLoading]="category.isLoading"
-    >
-      <a
-        [routerLinkActiveOptions]="{ exact: true }"
-        [routerLink]="category.path | formatpath"
-        class="clr-treenode-link category"
-        routerLinkActive="active"
-      >
-        <span>
-          <clr-icon
-            [attr.shape]="itemIcon(category) | default: 'folder'"
-          ></clr-icon>
-          {{ category.title }}
-        </span>
-      </a>
+        <ng-container *ngIf="section.children?.length > 0; else noChildCollapsed">
+          <a clrVerticalNavLink
+             (click)="openPopup(i)"
+             [ngClass]= "{'item-selected' : i == this.lastSelection}"
+             (mouseover)="flyoutIndex= i; lastSelection=i"
+          >
+            <clr-icon [attr.shape]="section.iconName | default:'home'" clrVerticalNavIcon></clr-icon>
+          </a>
 
-      <clr-tree-node
-        *ngFor="let entry of category.children; trackBy: identifyNavigationItem"
-        [clrLoading]="entry.isLoading"
-      >
-        <a
-          [routerLink]="entry.path | formatpath"
-          class="clr-treenode-link item"
-          routerLinkActive="active"
-        >
-          <clr-icon
-            [attr.shape]="itemIcon(entry) | default: 'folder'"
-          ></clr-icon>
-          <span>
-            {{ entry.title }}
-          </span>
-        </a>
-      </clr-tree-node>
-    </clr-tree-node>
-  </clr-tree-node>
-</clr-tree>
+          <ng-container *ngIf="shouldExpand(i)">
+            <div class="flyout nav-group-children"
+                 (mouseleave)="flyoutIndex= -1; lastSelection= -1"
+            >
+              <span class="flyout-header">{{ section.title }}</span>
+              <clr-vertical-nav-group-children class="flyout-group">
+                <a
+                  *ngFor="let category of section.children; trackBy: identifyNavigationItem"
+                  [routerLink]="formatPath(category.path)"
+                  [routerLinkActiveOptions]="{exact: true}"
+                  routerLinkActive="active"
+                  class="flyout-item nav-link"
+                  (click)="setNavState(false, i)"
+                >
+                {{ category.title }}
+                </a>
+              </clr-vertical-nav-group-children>
+            </div>
+          </ng-container>
+        </ng-container>
+
+        <ng-template #noChildCollapsed>
+              <span class="tooltip-override tooltip tooltip-md">
+                <a clrVerticalNavLink
+                   [ngClass]= "{'item-selected' : i == this.lastSelection}"
+                   [routerLinkActiveOptions]="{exact: true}"
+                   [routerLink]="formatPath(section.path)"
+                   routerLinkActive="active"
+                   (mouseover)="flyoutIndex= -1; lastSelection= i"
+                   (mouseout)="lastSelection= -1"
+                   (click)="closePopups(i)"
+                >
+                    <clr-icon [attr.shape]="section.iconName | default:'home'" clrVerticalNavIcon></clr-icon>
+                </a>
+                <span class="tooltip-content-override tooltip-content">
+                  <span>{{ section.title }}</span>
+                </span>
+              </span>
+        </ng-template>
+
+      </ng-container>
+      <!-- Expanded Navigation -->
+      <ng-template #expanded>
+        <ng-template [ngIf]="section.module">
+          <div class="nav-divider"></div>
+          <span class="title">{{ section.module }}</span>
+        </ng-template>
+        <ng-container *ngIf="section.children?.length > 0; else noChildExpanded">
+          <clr-vertical-nav-group
+            routerLinkActive="active"
+            [clrVerticalNavGroupExpanded]="shouldExpand(i)"
+            (clrVerticalNavGroupExpandedChange)="setNavState($event, i)"
+          >
+            <clr-icon [attr.shape]="section.iconName | default:'home'" clrVerticalNavIcon></clr-icon>
+          {{ section.title }}
+            <clr-vertical-nav-group-children>
+              <a
+                clrVerticalNavLink
+                *ngFor="let category of section.children; trackBy: identifyNavigationItem"
+                [routerLink]="formatPath(category.path)"
+                [routerLinkActiveOptions]="{exact: true}"
+                routerLinkActive="active"
+              >
+                <clr-icon *ngIf="itemIcon(category) as categoryIcon" [attr.shape]="categoryIcon" clrVerticalNavIcon></clr-icon>
+              {{ category.title }}
+              </a>
+            </clr-vertical-nav-group-children>
+          </clr-vertical-nav-group>
+        </ng-container>
+        <ng-template #noChildExpanded>
+          <a clrVerticalNavLink
+             (click)="closePopups(i)"
+             [routerLinkActiveOptions]="{exact: true}"
+             [routerLink]="formatPath(section.path)"
+             routerLinkActive="active"
+          >
+            <clr-icon [attr.shape]="section.iconName | default:'home'" clrVerticalNavIcon></clr-icon>
+            <div class="nav-item-text">
+            {{ section.title }}
+            </div>
+          </a>
+        </ng-template>
+      </ng-template>
+    </ng-container>
+  </div>
+
+  <!-- Theme switcher -->
+  <div class="theme-switcher">
+    <app-theme-switch-button [collapsed]="collapsed"></app-theme-switch-button>
+  </div>
+</clr-vertical-nav>
diff --git a/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.scss b/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.scss
index af34335d..d2a8b975 100644
--- a/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.scss
@@ -2,18 +2,146 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-.clr-treenode-link {
-  white-space: nowrap;
+:host-context(body) {
+  --selection-color: var(--clr-color-neutral-400);
+  --tooltip-text-color: black;
+  --flyout-background-color: var(--clr-color-neutral-50);
+  --flyout-color: var(--clr-global-font-color);
 }
 
-clr-tree-node.section {
-  margin-bottom: 18px;
+:host-context(body.dark) {
+  --selection-color: #324f62;
+  --tooltip-text-color: #EAEDEF;
+  --flyout-background-color: #1b2a32;
+  --flyout-color: #acbac3;
 }
 
-a.section {
-  font-weight: 600;
+.nav-group.is-expanded .nav-link .nav-text {
+  background-color: hotpink;
+  margin-left: 1.5rem;
 }
 
-a.category {
-  font-weight: 600;
+.clr-vertical-nav {
+  height: 100%;
+  background: none;
+  width: 300px;
+  transition: none;
+  will-change: none;
+
+  .title {
+    display: block;
+    margin-bottom: 0.25rem;
+    font-weight: 500;
+    font-size: 0.9em;
+    padding: 0 1rem 0 1rem;
+  }
+
+  .nav-divider {
+    border-top: 1px;
+  }
+
+  .nav-icon {
+    margin-top: auto;
+    margin-bottom: auto;
+    margin-left: 0.8rem;
+  }
+
+  .nav-item-text {
+    line-height: 1.5rem;
+  }
+
+  .nav-group:not(.is-expanded) {
+    height: 1.5rem;
+    min-height: 1.5rem;
+  }
+
+  .nav-group .nav-link {
+    padding-left: 2.25rem;
+  }
+
+  .nav-link {
+    height: 1.5rem;
+    padding-left: 0;
+  }
+
+  .namespace-switcher {
+    padding: 0 1rem 1rem 1rem;
+  }
+
+  .item-selected {
+    background-color: var(--selection-color);
+  }
+
+  .navigation-items {
+    flex-grow: 1;
+    overflow-y: auto;
+  }
+
+  .navigation-items-collapsed {
+    flex-grow: 1;
+    overflow-y: hidden;
+  }
+
+  .flyout-header {
+    font-weight: 700;
+    background-color: var(--selection-color);
+    border-radius: 0px 3px 0px 0px;
+    padding: 3px 8px !important;
+    margin-left: -1px;
+  }
+
+  .nav-divider {
+    border-color: #575757;
+    opacity: 0.2;
+  }
+
+  .flyout-item {
+    padding-left: 24px !important;
+    padding-right: 8px !important;
+    line-height: 1.5rem;
+  }
+
+  .flyout-group {
+    display: flex;
+    flex-direction: column;
+  }
+
+  .flyout {
+    display: flex;
+    border: 1px solid var(--navigation-border-color);
+    border-radius: 0px 4px 4px 0px;
+    flex-direction: column;
+    z-index: 502;
+    position: fixed;
+    margin-top: -31px;
+    margin-left: 48px;
+    background-color: var(--flyout-background-color, #1b2a32);
+    color: var(--flyout-color);
+    padding: 0px 0px 0px 0px;
+  }
+
+  .tooltip-override {
+    position: static;
+    display: block;
+  }
+
+  .tooltip-content-override:before {
+    border-left-color: var(--selection-color);
+    border-top-color: var(--selection-color);
+  }
+
+  .tooltip-content-override {
+    color: var(--tooltip-text-color);
+    background-color: var(--selection-color);
+    z-index: 503;
+    position: fixed;
+    bottom: unset;
+    left: unset;
+    margin: unset;
+    margin-top: -72px;
+    margin-left: 52px;
+    width: auto;
+    height: auto;
+    padding: 6px 9px;
+  }
 }
diff --git a/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.spec.ts b/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.spec.ts
index 8f129a99..70bd2e70 100644
--- a/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.spec.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.spec.ts
@@ -6,6 +6,8 @@ import { DefaultPipe } from '../../../../shared/pipes/default/default.pipe';
 import { NavigationComponent } from './navigation.component';
 import { NamespaceComponent } from '../namespace/namespace.component';
 import { NgSelectModule } from '@ng-select/ng-select';
+import { ThemeSwitchButtonComponent } from '../theme-switch/theme-switch-button.component';
+import { MonacoEditorConfig, MonacoProviderService } from 'ng-monaco-editor';
 
 describe('NavigationComponent', () => {
   let component: NavigationComponent;
@@ -13,8 +15,14 @@ describe('NavigationComponent', () => {
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
+      providers: [MonacoProviderService, MonacoEditorConfig],
       imports: [NgSelectModule],
-      declarations: [NavigationComponent, NamespaceComponent, DefaultPipe],
+      declarations: [
+        NavigationComponent,
+        NamespaceComponent,
+        DefaultPipe,
+        ThemeSwitchButtonComponent,
+      ],
     }).compileComponents();
   }));
 
diff --git a/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.ts b/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.ts
index ea53ad15..5b3f4ebb 100644
--- a/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/navigation/navigation.component.ts
@@ -8,7 +8,7 @@ import {
   OnDestroy,
   OnInit,
 } from '@angular/core';
-import { BehaviorSubject, Subscription } from 'rxjs';
+import { Subscription } from 'rxjs';
 import { Navigation, NavigationChild } from '../../../models/navigation';
 import { IconService } from '../../../../shared/services/icon/icon.service';
 import { NavigationService } from '../../../../shared/services/navigation/navigation.service';
@@ -25,8 +25,10 @@ const emptyNavigation: Navigation = {
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class NavigationComponent implements OnInit, OnDestroy {
-  behavior = new BehaviorSubject<Navigation>(emptyNavigation);
-
+  collapsed = false;
+  navExpandedState: any;
+  lastSelection: number;
+  flyoutIndex = -1;
   navigation = emptyNavigation;
 
   private navigationSubscription: Subscription;
@@ -46,6 +48,32 @@ export class NavigationComponent implements OnInit, OnDestroy {
         }
       }
     );
+    this.navigationSubscription = this.navigationService.lastSelection.subscribe(
+      selection => {
+        if (this.lastSelection !== selection) {
+          this.lastSelection = selection;
+          this.cd.markForCheck();
+        }
+      }
+    );
+
+    this.navigationSubscription = this.navigationService.expandedState.subscribe(
+      state => {
+        if (this.navExpandedState !== state) {
+          this.navExpandedState = state;
+          this.cd.markForCheck();
+        }
+      }
+    );
+
+    this.navigationSubscription = this.navigationService.collapsed.subscribe(
+      col => {
+        if (this.collapsed !== col) {
+          this.collapsed = col;
+          this.cd.markForCheck();
+        }
+      }
+    );
   }
 
   ngOnDestroy(): void {
@@ -59,4 +87,69 @@ export class NavigationComponent implements OnInit, OnDestroy {
   itemIcon(item: NavigationChild): string {
     return this.iconService.load(item);
   }
+
+  formatPath(path: string): string {
+    if (!path.startsWith('/')) {
+      return '/' + path;
+    }
+
+    return path;
+  }
+
+  openPopup(index: number) {
+    this.clearExpandedState();
+    this.setNavState(true, index);
+    this.setLastSelection(index);
+  }
+
+  closePopups(index) {
+    this.clearExpandedState();
+    this.flyoutIndex = -1;
+    this.setLastSelection(index);
+  }
+
+  setLastSelection(index) {
+    this.lastSelection = index;
+    this.navigationService.lastSelection.next(index);
+  }
+
+  setExpandedState(index, state) {
+    this.navExpandedState[index] = state;
+    this.navigationService.expandedState.next(this.navExpandedState);
+  }
+
+  clearExpandedState() {
+    this.navExpandedState = {};
+    this.navigationService.expandedState.next(this.navExpandedState);
+  }
+
+  setNavState($event, state: number) {
+    if (this.collapsed) {
+      this.setLastSelection(state);
+    } else {
+      this.setExpandedState(state, $event);
+      if ($event && this.lastSelection !== state) {
+        // collapse previously selected group
+        if (this.lastSelection) {
+          this.setExpandedState(this.lastSelection, false);
+        }
+        this.setLastSelection(state);
+      }
+    }
+  }
+
+  shouldExpand(index: number) {
+    if (this.collapsed) {
+      return index === this.flyoutIndex;
+    } else if (index.toString() in this.navExpandedState) {
+      return this.navExpandedState[index];
+    }
+    return false;
+  }
+
+  updateNavCollapsed(value: boolean): void {
+    this.collapsed = value;
+    this.navigationService.collapsed.next(value);
+    this.setExpandedState(this.lastSelection, false);
+  }
 }
diff --git a/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.html b/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.html
index 3b15033b..0ee193c5 100644
--- a/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.html
@@ -1,10 +1,13 @@
 <div class="switch-button-container">
-  <button
-    id="switchButton"
-    class="btn btn-link btn-inverse"
-    (click)="switchTheme()"
-  >
-    <clr-icon [attr.shape]="lightThemeEnabled ? 'moon' : 'sun'"></clr-icon>
-    {{ lightThemeEnabled ? 'dark' : 'light' }}
-  </button>
+  <ng-container *ngIf="collapsed; else expanded">
+    <button id="switchButton" class="btn btn-link btn-inverse btn-collapsed" (click)="switchTheme()">
+      <clr-icon [attr.shape]="lightThemeEnabled ? 'moon' : 'sun'"></clr-icon>
+    </button>
+  </ng-container>
+<ng-template #expanded>
+<button id="switchButton" class="btn btn-link btn-inverse" (click)="switchTheme()">
+  <clr-icon [attr.shape]="lightThemeEnabled ? 'moon' : 'sun'"></clr-icon>
+{{ lightThemeEnabled ? 'dark' : 'light' }}
+</button>
+</ng-template>
 </div>
diff --git a/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.scss b/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.scss
index 20c34e37..fcfa2565 100644
--- a/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.scss
@@ -1,5 +1,27 @@
 .switch-button-container {
-  height: 100%;
-  display: flex;
-  align-items: center;
+  padding: 0 0.5rem 0 0.5rem;
+
+  :host-context(body) {
+    --font-color: #0079b8;
+  }
+
+  :host-context(body.dark) {
+    --font-color: #49afd9;
+  }
+
+  .btn-collapsed {
+    padding-left: 4px;
+    padding-right: 4px;
+    min-width: 36px;
+    height: 36px;
+    margin-left: -4px;
+  }
+
+  button.btn.btn-link.btn-inverse {
+    color: var(--font-color);
+
+    &:active {
+      box-shadow: none;
+    }
+  }
 }
diff --git a/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.ts b/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.ts
index 0df40833..35090d01 100644
--- a/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/theme-switch/theme-switch-button.component.ts
@@ -1,7 +1,7 @@
 // Copyright (c) 2019 VMware, Inc. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 //
-import { Component, OnInit, Renderer2 } from '@angular/core';
+import { Component, Input, OnInit, Renderer2 } from '@angular/core';
 import { ThemeService, ThemeType } from './theme-switch.service';
 import { MonacoProviderService } from 'ng-monaco-editor';
 
@@ -13,6 +13,7 @@ import { MonacoProviderService } from 'ng-monaco-editor';
 })
 export class ThemeSwitchButtonComponent implements OnInit {
   themeType: ThemeType;
+  @Input() public collapsed: boolean;
 
   lightThemeEnabled: boolean;
 
diff --git a/web/src/app/modules/sugarloaf/models/navigation.ts b/web/src/app/modules/sugarloaf/models/navigation.ts
index 1cae9bb6..212d68bc 100644
--- a/web/src/app/modules/sugarloaf/models/navigation.ts
+++ b/web/src/app/modules/sugarloaf/models/navigation.ts
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 export interface NavigationChild {
+  module?: string;
   title: string;
   path: string;
   children?: NavigationChild[];
diff --git a/web/src/styles.scss b/web/src/styles.scss
index 1b9f2062..3d91225a 100644
--- a/web/src/styles.scss
+++ b/web/src/styles.scss
@@ -25,3 +25,7 @@ clr-datagrid .datagrid-outer-wrapper {
 .datagrid-table .datagrid-header {
   position: initial;
 }
+
+.clr-vertical-nav .nav-btn {
+  display: none;
+}


Test patch:
diff --git a/internal/printer/persistentvolume_test.go b/internal/printer/persistentvolume_test.go
deleted file mode 100644
index 5d2061c6..00000000
--- a/internal/printer/persistentvolume_test.go
+++ /dev/null
@@ -1,605 +0,0 @@
-/*
-Copyright (c) 2020 the Octant contributors. All Rights Reserved.
-SPDX-License-Identifier: Apache-2.0
-*/
-
-package printer
-
-import (
-	"context"
-	"testing"
-
-	"github.com/golang/mock/gomock"
-	"github.com/stretchr/testify/require"
-	"github.com/vmware-tanzu/octant/internal/testutil"
-	"github.com/vmware-tanzu/octant/pkg/store"
-	"github.com/vmware-tanzu/octant/pkg/view/component"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-func Test_PersistentVolumeListHandler(t *testing.T) {
-	cols := component.NewTableCols("Name", "Capacity", "Access Modes", "Reclaim Policy", "Status", "Claim", "Storage Class", "Reason", "Age")
-	now := testutil.Time()
-
-	pvcObject := testutil.CreatePersistentVolumeClaim("pvc")
-
-	object := testutil.CreatePersistentVolume("persistentVolume")
-	object.CreationTimestamp = metav1.Time{Time: now}
-	object.Spec.ClaimRef = &corev1.ObjectReference{
-		APIVersion: pvcObject.APIVersion,
-		Kind:       pvcObject.Kind,
-		Name:       pvcObject.Name,
-		Namespace:  pvcObject.Namespace,
-	}
-
-	list := &corev1.PersistentVolumeList{
-		Items: []corev1.PersistentVolume{*object},
-	}
-
-	cases := []struct {
-		name     string
-		list     *corev1.PersistentVolumeList
-		expected *component.Table
-		isErr    bool
-	}{
-		{
-			name: "in general",
-			list: list,
-			expected: component.NewTableWithRows("Persistent Volumes", "We couldn't find any persistent volumes!", cols,
-				[]component.TableRow{
-					{
-						"Name":           component.NewLink("", "persistentVolume", "/persistentVolume"),
-						"Capacity":       component.NewText("0"),
-						"Access Modes":   component.NewText(""),
-						"Reclaim Policy": component.NewText(""),
-						"Status":         component.NewText("Bound"),
-						"Claim":          component.NewLink("", "namespace/pvc", "/pvc"),
-						"Storage Class":  component.NewText(""),
-						"Reason":         component.NewText(""),
-						"Age":            component.NewTimestamp(now),
-					},
-				}),
-		},
-		{
-			name:  "list is nil",
-			list:  nil,
-			isErr: true,
-		},
-	}
-
-	for _, tc := range cases {
-		t.Run(tc.name, func(t *testing.T) {
-			controller := gomock.NewController(t)
-			defer controller.Finish()
-
-			tpo := newTestPrinterOptions(controller)
-			printOptions := tpo.ToOptions()
-
-			ctx := context.Background()
-
-			if tc.list != nil {
-				tpo.PathForObject(&tc.list.Items[0], tc.list.Items[0].Name, "/persistentVolume")
-
-				pvcKey, err := store.KeyFromObject(pvcObject)
-				require.NoError(t, err)
-
-				tpo.objectStore.EXPECT().Get(ctx, pvcKey).
-					Return(testutil.ToUnstructured(t, pvcObject), nil)
-
-				tpo.PathForObject(pvcObject, pvcObject.Namespace+"/"+pvcObject.Name, "/pvc")
-			}
-			got, err := PersistentVolumeListHandler(ctx, tc.list, printOptions)
-			if tc.isErr {
-				require.Error(t, err)
-				return
-			}
-			require.NoError(t, err)
-
-			component.AssertEqual(t, tc.expected, got)
-		})
-	}
-}
-
-func Test_PersistentVolumeConfiguration(t *testing.T) {
-	persistentVolume := testutil.CreatePersistentVolume("persistentVolume")
-	persistentVolume.Spec.PersistentVolumeSource = corev1.PersistentVolumeSource{
-		HostPath: &corev1.HostPathVolumeSource{
-			Path: "/private/tmp",
-		},
-	}
-
-	cases := []struct {
-		name             string
-		persistentVolume *corev1.PersistentVolume
-		expected         component.Component
-		isErr            bool
-	}{
-		{
-			name:             "host path",
-			persistentVolume: persistentVolume,
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Reclaim Policy",
-					Content: component.NewText(""),
-				},
-				{
-					Header:  "Storage Class",
-					Content: component.NewText(""),
-				},
-				{
-					Header:  "Access Modes",
-					Content: component.NewText(""),
-				},
-				{
-					Header:  "Capacity",
-					Content: component.NewText("0"),
-				},
-				{
-					Header:  "Host Path",
-					Content: component.NewText("{\"path\":\"/private/tmp\"}"),
-				},
-			}...),
-		},
-		{
-			name:             "nil persistentVolume",
-			persistentVolume: nil,
-			isErr:            true,
-		},
-	}
-
-	for _, tc := range cases {
-		t.Run(tc.name, func(t *testing.T) {
-			controller := gomock.NewController(t)
-			defer controller.Finish()
-
-			tpo := newTestPrinterOptions(controller)
-			printOptions := tpo.ToOptions()
-
-			pc := NewPersistentVolumeConfiguration(tc.persistentVolume)
-
-			summary, err := pc.Create(printOptions)
-			if tc.isErr {
-				require.Error(t, err)
-				return
-			}
-			require.NoError(t, err)
-
-			component.AssertEqual(t, tc.expected, summary)
-		})
-	}
-}
-
-func Test_printPersistentVolumeSource(t *testing.T) {
-	cases := []struct {
-		name             string
-		persistentVolume corev1.PersistentVolume
-		expected         *component.Summary
-	}{
-		{
-			name: "gce persistent disk",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						GCEPersistentDisk: &corev1.GCEPersistentDiskVolumeSource{
-							PDName: "my-data-disk",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "GCE Persistent Disk",
-					Content: component.NewText("{\"pdName\":\"my-data-disk\"}"),
-				},
-			}...),
-		},
-		{
-			name: "aws ebs",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						AWSElasticBlockStore: &corev1.AWSElasticBlockStoreVolumeSource{
-							VolumeID: "vol-0b402cb854e070fad",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "AWS Elastic Block Store",
-					Content: component.NewText("{\"volumeID\":\"vol-0b402cb854e070fad\"}"),
-				},
-			}...),
-		},
-		{
-			name: "host path",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						HostPath: &corev1.HostPathVolumeSource{
-							Path: "/private/tmp",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Host Path",
-					Content: component.NewText("{\"path\":\"/private/tmp\"}"),
-				},
-			}...),
-		},
-		{
-			name: "glusterfs",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						Glusterfs: &corev1.GlusterfsPersistentVolumeSource{
-							EndpointsName: "192.168.122.221:1",
-							Path:          "kube_vol",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "GlusterFS",
-					Content: component.NewText("{\"endpoints\":\"192.168.122.221:1\",\"path\":\"kube_vol\"}"),
-				},
-			}...),
-		},
-		{
-			name: "nfs",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						NFS: &corev1.NFSVolumeSource{
-							Server: "nfs.example.com",
-							Path:   "/share1",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "NFS",
-					Content: component.NewText("{\"server\":\"nfs.example.com\",\"path\":\"/share1\"}"),
-				},
-			}...),
-		},
-		{
-			name: "rbd",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						RBD: &corev1.RBDPersistentVolumeSource{
-							CephMonitors: []string{"10.16.154.78:6789", "10.16.154.82:6789"},
-							RBDImage:     "foo",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "RBD",
-					Content: component.NewText("{\"monitors\":[\"10.16.154.78:6789\",\"10.16.154.82:6789\"],\"image\":\"foo\"}"),
-				},
-			}...),
-		},
-		{
-			name: "isci",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						ISCSI: &corev1.ISCSIPersistentVolumeSource{
-							TargetPortal: "10.0.2.15:3260",
-							IQN:          "iqn.2001-04.com.example:storage.kube.sys1.xyz",
-							Lun:          0,
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "ISCI",
-					Content: component.NewText("{\"targetPortal\":\"10.0.2.15:3260\",\"iqn\":\"iqn.2001-04.com.example:storage.kube.sys1.xyz\",\"lun\":0}"),
-				},
-			}...),
-		},
-		{
-			name: "cinder",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						Cinder: &corev1.CinderPersistentVolumeSource{
-							VolumeID: "573e024d-5235-49ce-8332-be1576d323f8",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Cinder",
-					Content: component.NewText("{\"volumeID\":\"573e024d-5235-49ce-8332-be1576d323f8\"}"),
-				},
-			}...),
-		},
-		{
-			name: "cephfs",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						CephFS: &corev1.CephFSPersistentVolumeSource{
-							Monitors: []string{"10.16.154.78:6789", "10.16.154.82:6789"},
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "CephFS",
-					Content: component.NewText("{\"monitors\":[\"10.16.154.78:6789\",\"10.16.154.82:6789\"]}"),
-				},
-			}...),
-		},
-		{
-			name: "fc",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						FC: &corev1.FCVolumeSource{
-							FSType: "ext4",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "FC",
-					Content: component.NewText("{\"fsType\":\"ext4\"}"),
-				},
-			}...),
-		},
-		{
-			name: "flocker",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						Flocker: &corev1.FlockerVolumeSource{
-							DatasetName: "my-flocker-vol",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Flocker",
-					Content: component.NewText("{\"datasetName\":\"my-flocker-vol\"}"),
-				},
-			}...),
-		},
-		{
-			name: "flex volume",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						FlexVolume: &corev1.FlexPersistentVolumeSource{
-							Driver: "kubernetes.io/lvm",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Flex Volume",
-					Content: component.NewText("{\"driver\":\"kubernetes.io/lvm\"}"),
-				},
-			}...),
-		},
-		{
-			name: "azure file",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						AzureFile: &corev1.AzureFilePersistentVolumeSource{
-							SecretName: "azure-secret",
-							ShareName:  "k8stest",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Azure File",
-					Content: component.NewText("{\"secretName\":\"azure-secret\",\"shareName\":\"k8stest\",\"secretNamespace\":null}"),
-				},
-			}...),
-		},
-		{
-			name: "vsphere volume",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						VsphereVolume: &corev1.VsphereVirtualDiskVolumeSource{
-							VolumePath: "/private/tmp",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Vsphere Volume",
-					Content: component.NewText("{\"volumePath\":\"/private/tmp\"}"),
-				},
-			}...),
-		},
-		{
-			name: "quobyte",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						Quobyte: &corev1.QuobyteVolumeSource{
-							Registry: "registry:7861",
-							Volume:   "testVolume",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Quobyte",
-					Content: component.NewText("{\"registry\":\"registry:7861\",\"volume\":\"testVolume\"}"),
-				},
-			}...),
-		},
-		{
-			name: "azure disk",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						AzureDisk: &corev1.AzureDiskVolumeSource{
-							DiskName:    "test.vhd",
-							DataDiskURI: "https://someaccount.blob.microsoft.net/vhds/test.vhd",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Azure Disk",
-					Content: component.NewText("{\"diskName\":\"test.vhd\",\"diskURI\":\"https://someaccount.blob.microsoft.net/vhds/test.vhd\"}"),
-				},
-			}...),
-		},
-		{
-			name: "photon persistent disk",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						PhotonPersistentDisk: &corev1.PhotonPersistentDiskVolumeSource{
-							PdID:   "e8dbc38c-9374-4099-8a09-fcf4be0a7641",
-							FSType: "ext4",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Photon Persistent Disk",
-					Content: component.NewText("{\"pdID\":\"e8dbc38c-9374-4099-8a09-fcf4be0a7641\",\"fsType\":\"ext4\"}"),
-				},
-			}...),
-		},
-		{
-			name: "portworx",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						PortworxVolume: &corev1.PortworxVolumeSource{
-							VolumeID: "vol1",
-							FSType:   "ext4",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Portworx Volume",
-					Content: component.NewText("{\"volumeID\":\"vol1\",\"fsType\":\"ext4\"}"),
-				},
-			}...),
-		},
-		{
-			name: "scaleio",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						ScaleIO: &corev1.ScaleIOPersistentVolumeSource{
-							Gateway:    "https://localhost:443/api",
-							System:     "scaleio",
-							VolumeName: "vol-0",
-							SecretRef: &corev1.SecretReference{
-								Name: "sio-secret",
-							},
-							FSType: "xfs",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "ScaleIO",
-					Content: component.NewText("{\"gateway\":\"https://localhost:443/api\",\"system\":\"scaleio\",\"secretRef\":{\"name\":\"sio-secret\"},\"volumeName\":\"vol-0\",\"fsType\":\"xfs\"}"),
-				},
-			}...),
-		},
-		{
-			name: "local",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						Local: &corev1.LocalVolumeSource{
-							Path: "/private/tmp",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "Local",
-					Content: component.NewText("{\"path\":\"/private/tmp\"}"),
-				},
-			}...),
-		},
-		{
-			name: "storageos",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						StorageOS: &corev1.StorageOSPersistentVolumeSource{
-							VolumeName: "vol-0",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "StorageOS",
-					Content: component.NewText("{\"volumeName\":\"vol-0\"}"),
-				},
-			}...),
-		},
-		{
-			name: "csi",
-			persistentVolume: corev1.PersistentVolume{
-				Spec: corev1.PersistentVolumeSpec{
-					PersistentVolumeSource: corev1.PersistentVolumeSource{
-						CSI: &corev1.CSIPersistentVolumeSource{
-							Driver:       "csi-driver.example.com",
-							VolumeHandle: "existingVolumeName",
-						},
-					},
-				},
-			},
-			expected: component.NewSummary("Configuration", []component.SummarySection{
-				{
-					Header:  "CSI",
-					Content: component.NewText("{\"driver\":\"csi-driver.example.com\",\"volumeHandle\":\"existingVolumeName\"}"),
-				},
-			}...),
-		},
-	}
-
-	for _, tc := range cases {
-		t.Run(tc.name, func(t *testing.T) {
-			var sections component.SummarySections
-			sections, err := printPersistentVolumeSource(&tc.persistentVolume, sections)
-			require.NoError(t, err)
-
-			got := component.NewSummary("Configuration", sections...)
-
-			component.AssertEqual(t, tc.expected, got)
-		})
-	}
-}
diff --git a/internal/printer/persistentvolumeclaim_test.go b/internal/printer/persistentvolumeclaim_test.go
index fbbd5ae3..d8afc296 100644
--- a/internal/printer/persistentvolumeclaim_test.go
+++ b/internal/printer/persistentvolumeclaim_test.go
@@ -22,7 +22,7 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
-func Test_PersistentVolumeClaimListHandler(t *testing.T) {
+func Test_PersistentVolumeListHandler(t *testing.T) {
 	labels := map[string]string{
 		"foo": "bar",
 	}
diff --git a/pkg/navigation/navigation_test.go b/pkg/navigation/navigation_test.go
index 39b8d91c..9d8de9e2 100644
--- a/pkg/navigation/navigation_test.go
+++ b/pkg/navigation/navigation_test.go
@@ -7,7 +7,6 @@ package navigation
 
 import (
 	"context"
-	"fmt"
 	"path"
 	"testing"
 
@@ -38,22 +37,22 @@ func Test_NewNavigation(t *testing.T) {
 func TestEntriesHelper(t *testing.T) {
 	neh := EntriesHelper{}
 
-	neh.Add("title", "suffix", icon.OverviewService, false)
+	neh.Add("title", "suffix", false)
 
-	list, err := neh.Generate("/prefix")
+	list, err := neh.Generate("/prefix", "", "")
 	require.NoError(t, err)
 
 	expected := Navigation{
 		Title:    "title",
 		Path:     path.Join("/prefix", "suffix"),
-		IconName: fmt.Sprintf("internal:%s", icon.OverviewService),
+		IconName: "",
 	}
 
 	assert.Len(t, list, 1)
 	assert.Equal(t, expected.Title, list[0].Title)
 	assert.Equal(t, expected.Path, list[0].Path)
 	assert.Equal(t, expected.IconName, list[0].IconName)
-	assert.NotEmpty(t, list[0].IconSource)
+	assert.Empty(t, list[0].IconSource)
 }
 
 func TestCRDEntries_namespace_scoped(t *testing.T) {
@@ -103,19 +102,26 @@ func TestCRDEntries_namespace_scoped(t *testing.T) {
 	require.NoError(t, err)
 
 	namespaceExpected := []Navigation{
+		{
+			Title: "Overview",
+			Path:  "/prefix",
+		},
 		createNavForCR(t, namespaceCR.GetName()),
 	}
 
 	assert.Equal(t, namespaceExpected, namespaceGot)
 
-	clusterGot, _, err := CRDEntries(ctx, "/prefix", "default", objectStore, true)
-	require.NoError(t, err)
-
-	clusterExpected := []Navigation{
-		createNavForCR(t, clusterCR.GetName()),
-	}
-
-	assert.Equal(t, clusterExpected, clusterGot)
+	//clusterGot, _, err := CRDEntries(ctx, "/prefix", "default", objectStore, true)
+	//require.NoError(t, err)
+	//
+	//clusterExpected := []Navigation{
+	//	{
+	//		Title: "Overview",
+	//		Path:  "/prefix",
+	//	},
+	//}
+
+	//assert.Equal(t, clusterExpected, clusterGot)
 }
 
 func createNavForCR(t *testing.T, name string) Navigation {
diff --git a/pkg/octant/handler_test.go b/pkg/octant/handler_test.go
deleted file mode 100644
index 0603786d..00000000
--- a/pkg/octant/handler_test.go
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- *  Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- *  SPDX-License-Identifier: Apache-2.0
- *
- */
-
-package octant
-
-import (
-	"context"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"net/http"
-	"net/http/httptest"
-	"net/url"
-	"path"
-	"testing"
-
-	"github.com/stretchr/testify/require"
-)
-
-func TestHandlerFactory_Handler(t *testing.T) {
-	frontend := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprint(w, "frontend")
-	})
-
-	backend := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprint(w, "backend")
-	})
-
-	proxied := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprint(w, "proxied")
-	})
-
-	proxiedTs := httptest.NewServer(proxied)
-	defer proxiedTs.Close()
-
-	tests := []struct {
-		name         string
-		options      []Option
-		wantFrontend string
-		wantBackend  string
-		wantErr      bool
-	}{
-		{
-			name: "wire backend and frontend by default",
-			options: []Option{
-				func(o *options) {
-					o.frontendHandler = func(ctx context.Context) (handler http.Handler, err error) {
-						return frontend, nil
-					}
-					o.backendHandler = func(ctx context.Context) (handler http.Handler, err error) {
-						return backend, nil
-					}
-				},
-			},
-			wantFrontend: "frontend",
-			wantBackend:  "backend",
-		},
-		{
-			name: "proxy frontend",
-			options: []Option{
-				func(o *options) {
-					o.backendHandler = func(ctx context.Context) (handler http.Handler, err error) {
-						return backend, nil
-					}
-				},
-				FrontendURL(proxiedTs.URL),
-			},
-			wantFrontend: "proxied",
-			wantBackend:  "backend",
-		},
-		{
-			name: "frontend factory returns an error",
-			options: []Option{
-				func(o *options) {
-					o.frontendHandler = func(ctx context.Context) (handler http.Handler, err error) {
-						return nil, fmt.Errorf("error")
-					}
-					o.backendHandler = func(ctx context.Context) (handler http.Handler, err error) {
-						return backend, nil
-					}
-				},
-			},
-			wantErr: true,
-		},
-		{
-			name: "backend factory returns an error",
-			options: []Option{
-				func(o *options) {
-					o.frontendHandler = func(ctx context.Context) (handler http.Handler, err error) {
-						return frontend, nil
-					}
-					o.backendHandler = func(ctx context.Context) (handler http.Handler, err error) {
-						return nil, fmt.Errorf("error")
-					}
-				},
-			},
-			wantErr: true,
-		},
-	}
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			hf := NewHandlerFactory(test.options...)
-
-			ctx := context.Background()
-
-			h, err := hf.Handler(ctx)
-			if test.wantErr {
-				require.Error(t, err)
-				return
-			}
-			require.NoError(t, err)
-
-			ts := httptest.NewServer(h)
-			defer ts.Close()
-
-			frontendPath := genTestURL(t, ts.URL, "/")
-			backendPath := genTestURL(t, ts.URL, "api", "v1", "foo")
-
-			resFrontend, err := http.Get(frontendPath)
-			require.NoError(t, err)
-
-			resBackend, err := http.Get(backendPath)
-			require.NoError(t, err)
-
-			require.Equal(t, test.wantFrontend, string(readFromCloser(t, resFrontend.Body)))
-			require.Equal(t, test.wantBackend, string(readFromCloser(t, resBackend.Body)))
-		})
-	}
-}
-
-func TestNewProxiedFrontend(t *testing.T) {
-	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprint(w, "content")
-	})
-
-	ts := httptest.NewServer(handler)
-	defer ts.Close()
-
-	tests := []struct {
-		name      string
-		targetURL string
-		wantErr   bool
-	}{
-		{
-			name:      "in general",
-			targetURL: ts.URL,
-		},
-		{
-			name:      "URL without scheme",
-			targetURL: "example.com",
-		},
-		{
-			name:      "invalid URL",
-			targetURL: "%",
-			wantErr:   true,
-		},
-	}
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			ctx := context.Background()
-
-			_, err := NewProxiedFrontend(ctx, test.targetURL)
-			if test.wantErr {
-				require.Error(t, err)
-				return
-			}
-			require.NoError(t, err)
-		})
-	}
-}
-
-func readFromCloser(t *testing.T, rc io.ReadCloser) []byte {
-	data, err := ioutil.ReadAll(rc)
-	require.NoError(t, err)
-	return data
-}
-
-func genTestURL(t *testing.T, base string, parts ...string) string {
-	u, err := url.Parse(base)
-	require.NoError(t, err)
-
-	u.Path = path.Join(append([]string{u.Path}, parts...)...)
-	return u.String()
-}