Repository: vmware-tanzu/octant
Pull request #1290 (2020-08-25 18:23:41)

Add SendEvent support to JavaScript plugin runtime
Signed-off-by: Wayne Witzel III <wayne@riotousliving.com>


Enables JavaScript plugins to send events via the API to a websocket client.


wwitzel3: @scothis this should unblock your redirect issue in the plugin.

Patch:
diff --git a/build.go b/build.go
index 2383b0a3..7d795bf8 100644
--- a/build.go
+++ b/build.go
@@ -264,7 +264,7 @@ func build() {
 	if runtime.GOOS == "windows" {
 		artifact = "octant.exe"
 	}
-	runCmd("go", nil, "build", "-mod=vendor", "-o", "build/"+artifact, GO_FLAGS, "-v", "./cmd/octant")
+	runCmd("go", nil, "build", "-o", "build/"+artifact, GO_FLAGS, "-v", "./cmd/octant")
 }
 
 func runDev() {
diff --git a/internal/api/content_manager.go b/internal/api/content_manager.go
index cfb1198a..9b1b1933 100644
--- a/internal/api/content_manager.go
+++ b/internal/api/content_manager.go
@@ -7,8 +7,6 @@ package api
 
 import (
 	"context"
-	"crypto/sha256"
-	"encoding/json"
 	"errors"
 	"fmt"
 	"hash/fnv"
@@ -39,18 +37,9 @@ type ContentManagerOption func(manager *ContentManager)
 // if the action should be be immediately rerun.
 type ContentGenerateFunc func(ctx context.Context, state octant.State) (Content, bool, error)
 
-// Content is a content to be sent to clients.
 type Content struct {
-	// Response is a content response.
 	Response component.ContentResponse
-	// Path is the path of the content.
-	Path string
-}
-
-// Checksum return's the content's checksum.
-func (c *Content) Checksum() string {
-	b, _ := json.MarshalIndent(c, "", "  ")
-	return fmt.Sprintf("%s", sha256.Sum256(b))
+	Path     string
 }
 
 var (
@@ -113,24 +102,15 @@ func (cm *ContentManager) Start(ctx context.Context, state octant.State, s Octan
 		close(cm.updateContentCh)
 	}()
 
-	ctx, cancel := context.WithCancel(ctx)
-
 	updateCancel := state.OnContentPathUpdate(func(contentPath string) {
 		cm.updateContentCh <- struct{}{}
 	})
-
-	go func() {
-		<-s.StopCh()
-		updateCancel()
-		cancel()
-	}()
+	defer updateCancel()
 
 	cm.poller.Run(ctx, cm.updateContentCh, cm.runUpdate(state, s), event.DefaultScheduleDelay)
 }
 
 func (cm *ContentManager) runUpdate(state octant.State, s OctantClient) PollerFunc {
-	previousChecksum := ""
-
 	return func(ctx context.Context) bool {
 		contentPath := state.GetContentPath()
 		if contentPath == "" {
@@ -155,12 +135,6 @@ func (cm *ContentManager) runUpdate(state octant.State, s OctantClient) PollerFu
 			return false
 		}
 
-		checksum := content.Checksum()
-		if checksum == previousChecksum {
-			return false
-		}
-		previousChecksum = checksum
-
 		if ctx.Err() == nil {
 			if content.Path == state.GetContentPath() {
 				s.Send(CreateContentEvent(content.Response, state.GetNamespace(), contentPath, state.GetQueryParams()))
@@ -250,7 +224,8 @@ func (cm *ContentManager) SetNamespace(state octant.State, payload action.Payloa
 		return fmt.Errorf("extract namespace from payload: %w", err)
 	}
 	state.SetNamespace(namespace)
-	return state.Dispatch(cm.ctx, action.RequestSetNamespace, payload)
+	state.Dispatch(cm.ctx, action.RequestSetNamespace, payload)
+	return nil
 }
 
 // SetContentPath sets the current content path.
@@ -263,6 +238,8 @@ func (cm *ContentManager) SetContentPath(state octant.State, payload action.Payl
 		return fmt.Errorf("extract query params from payload: %w", err)
 	}
 
+	cm.logger.With("content-path", contentPath).Debugf("setting content path")
+
 	state.SetContentPath(contentPath)
 	return nil
 }
diff --git a/internal/api/fake/mock_client_manager.go b/internal/api/fake/mock_client_manager.go
index 60848892..0a871573 100644
--- a/internal/api/fake/mock_client_manager.go
+++ b/internal/api/fake/mock_client_manager.go
@@ -13,7 +13,7 @@ import (
 
 	api "github.com/vmware-tanzu/octant/internal/api"
 	config "github.com/vmware-tanzu/octant/internal/config"
-	api0 "github.com/vmware-tanzu/octant/pkg/plugin/api"
+	event "github.com/vmware-tanzu/octant/pkg/event"
 )
 
 // MockClientManager is a mock of ClientManager interface
@@ -69,10 +69,10 @@ func (mr *MockClientManagerMockRecorder) Clients() *gomock.Call {
 }
 
 // Get mocks base method
-func (m *MockClientManager) Get(arg0 string) api0.WSEventSender {
+func (m *MockClientManager) Get(arg0 string) event.WSEventSender {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Get", arg0)
-	ret0, _ := ret[0].(api0.WSEventSender)
+	ret0, _ := ret[0].(event.WSEventSender)
 	return ret0
 }
 
diff --git a/internal/api/fake/mock_octant_client.go b/internal/api/fake/mock_octant_client.go
index 14ff44fa..09f3a7cb 100644
--- a/internal/api/fake/mock_octant_client.go
+++ b/internal/api/fake/mock_octant_client.go
@@ -60,17 +60,3 @@ func (mr *MockOctantClientMockRecorder) Send(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockOctantClient)(nil).Send), arg0)
 }
-
-// StopCh mocks base method
-func (m *MockOctantClient) StopCh() <-chan struct{} {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "StopCh")
-	ret0, _ := ret[0].(<-chan struct{})
-	return ret0
-}
-
-// StopCh indicates an expected call of StopCh
-func (mr *MockOctantClientMockRecorder) StopCh() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopCh", reflect.TypeOf((*MockOctantClient)(nil).StopCh))
-}
diff --git a/internal/api/websocket_client.go b/internal/api/websocket_client.go
index 2be185e5..08151db9 100644
--- a/internal/api/websocket_client.go
+++ b/internal/api/websocket_client.go
@@ -53,7 +53,6 @@ type WebsocketClient struct {
 	state    octant.State
 	handlers map[string][]octant.ClientRequestHandler
 	id       uuid.UUID
-	stopCh   chan struct{}
 }
 
 var _ OctantClient = (*WebsocketClient)(nil)
@@ -75,7 +74,6 @@ func NewWebsocketClient(ctx context.Context, conn *websocket.Conn, manager *Webs
 		dashConfig: dashConfig,
 		logger:     logger,
 		handlers:   make(map[string][]octant.ClientRequestHandler),
-		stopCh:     make(chan struct{}, 1),
 	}
 
 	state := NewWebsocketState(dashConfig, actionDispatcher, client)
@@ -109,7 +107,6 @@ func NewTemporaryWebsocketClient(ctx context.Context, conn *websocket.Conn, mana
 		manager:  manager,
 		logger:   logger,
 		handlers: make(map[string][]octant.ClientRequestHandler),
-		stopCh:   make(chan struct{}, 1),
 	}
 
 	state := NewTemporaryWebsocketState(actionDispatcher, client)
@@ -167,8 +164,6 @@ func (c *WebsocketClient) readPump() {
 			c.logger.WithErr(err).Errorf("Handle websocket message")
 		}
 	}
-
-	close(c.stopCh)
 }
 
 func (c *WebsocketClient) handle(message []byte) error {
@@ -276,11 +271,6 @@ func (c *WebsocketClient) Send(ev event.Event) {
 	}
 }
 
-// StopCh returns the client's stop channel. It will be closed when the WebsocketClient is closed.
-func (c *WebsocketClient) StopCh() <-chan struct{} {
-	return c.stopCh
-}
-
 func (c *WebsocketClient) RegisterHandler(handler octant.ClientRequestHandler) {
 	c.handlers[handler.RequestType] = append(c.handlers[handler.RequestType], handler)
 }
diff --git a/internal/api/websocket_client_manager.go b/internal/api/websocket_client_manager.go
index 1b2ca16f..79ca78a5 100644
--- a/internal/api/websocket_client_manager.go
+++ b/internal/api/websocket_client_manager.go
@@ -9,7 +9,7 @@ import (
 	"context"
 	"net/http"
 
-	"github.com/vmware-tanzu/octant/pkg/plugin/api"
+	"github.com/vmware-tanzu/octant/pkg/event"
 
 	"github.com/google/uuid"
 
@@ -24,7 +24,7 @@ type ClientManager interface {
 	Clients() []*WebsocketClient
 	ClientFromRequest(dashConfig config.Dash, w http.ResponseWriter, r *http.Request) (*WebsocketClient, error)
 	TemporaryClientFromLoadingRequest(w http.ResponseWriter, r *http.Request) (*WebsocketClient, error)
-	Get(id string) api.WSEventSender
+	Get(id string) event.WSEventSender
 }
 
 type clientMeta struct {
@@ -145,7 +145,7 @@ func (m *WebsocketClientManager) TemporaryClientFromLoadingRequest(w http.Respon
 	return client, nil
 }
 
-func (m *WebsocketClientManager) Get(id string) api.WSEventSender {
+func (m *WebsocketClientManager) Get(id string) event.WSEventSender {
 	for _, client := range m.Clients() {
 		if id == client.ID() {
 			return client
diff --git a/internal/api/websocket_state.go b/internal/api/websocket_state.go
index 17e90c5d..cf33060c 100644
--- a/internal/api/websocket_state.go
+++ b/internal/api/websocket_state.go
@@ -56,7 +56,6 @@ func defaultStateManagers(clientID string, dashConfig config.Dash) []StateManage
 type OctantClient interface {
 	Send(event event.Event)
 	ID() string
-	StopCh() <-chan struct{}
 }
 
 type atomicString struct {
diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go
index 23b1c918..a3f1ea71 100644
--- a/internal/cluster/cluster.go
+++ b/internal/cluster/cluster.go
@@ -105,6 +105,11 @@ func newCluster(ctx context.Context, clientConfig clientcmd.ClientConfig, restCl
 		return nil, errors.Wrap(err, "create dynamic client")
 	}
 
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(restClient)
+	if err != nil {
+		return nil, errors.Wrap(err, "create discovery client")
+	}
+
 	dir, err := ioutil.TempDir("", "octant")
 	if err != nil {
 		return nil, errors.Wrap(err, "create temp directory")
@@ -129,7 +134,7 @@ func newCluster(ctx context.Context, clientConfig clientcmd.ClientConfig, restCl
 		restConfig:         restClient,
 		kubernetesClient:   kubernetesClient,
 		dynamicClient:      dynamicClient,
-		discoveryClient:    cachedDiscoveryClient,
+		discoveryClient:    discoveryClient,
 		restMapper:         restMapper,
 		logger:             internalLog.From(ctx),
 		defaultNamespace:   defaultNamespace,
diff --git a/pkg/dash/dash.go b/pkg/dash/dash.go
index dd7001ae..7741251a 100644
--- a/pkg/dash/dash.go
+++ b/pkg/dash/dash.go
@@ -257,7 +257,7 @@ func (r *Runner) initAPI(ctx context.Context, logger log.Logger, options Options
 		WebsocketClientManager: r.websocketClientManager,
 	}
 
-	pluginManager, err := initPlugin(moduleManager, r.actionManager, pluginDashboardService)
+	pluginManager, err := initPlugin(moduleManager, r.actionManager, r.websocketClientManager, pluginDashboardService)
 	if err != nil {
 		return nil, nil, fmt.Errorf("initializing plugin manager: %w", err)
 	}
diff --git a/pkg/dash/plugin.go b/pkg/dash/plugin.go
index 32bb0eb7..25e3787f 100644
--- a/pkg/dash/plugin.go
+++ b/pkg/dash/plugin.go
@@ -10,17 +10,18 @@ import (
 
 	"github.com/vmware-tanzu/octant/internal/module"
 	"github.com/vmware-tanzu/octant/pkg/action"
+	"github.com/vmware-tanzu/octant/pkg/event"
 	"github.com/vmware-tanzu/octant/pkg/plugin"
 	"github.com/vmware-tanzu/octant/pkg/plugin/api"
 )
 
-func initPlugin(moduleManager module.ManagerInterface, actionManager *action.Manager, service api.Service) (*plugin.Manager, error) {
+func initPlugin(moduleManager module.ManagerInterface, actionManager *action.Manager, ws event.WSClientGetter, service api.Service) (*plugin.Manager, error) {
 	apiService, err := api.New(service)
 	if err != nil {
 		return nil, fmt.Errorf("create dashboard api: %w", err)
 	}
 
-	m := plugin.NewManager(apiService, moduleManager, actionManager)
+	m := plugin.NewManager(apiService, moduleManager, actionManager, ws)
 
 	pluginList, err := plugin.AvailablePlugins(plugin.DefaultConfig)
 	if err != nil {
diff --git a/pkg/event/event.go b/pkg/event/event.go
index cd5bd5cd..769ccadc 100644
--- a/pkg/event/event.go
+++ b/pkg/event/event.go
@@ -5,6 +5,8 @@ SPDX-License-Identifier: Apache-2.0
 
 package event
 
+//go:generate mockgen -destination=./fake/mock_event.go -package=fake github.com/vmware-tanzu/octant/pkg/event WSClientGetter,WSEventSender
+
 import (
 	"fmt"
 
@@ -93,3 +95,11 @@ func CreateEvent(eventType EventType, fields action.Payload) Event {
 		Data: fields,
 	}
 }
+
+type WSClientGetter interface {
+	Get(id string) WSEventSender
+}
+
+type WSEventSender interface {
+	Send(event Event)
+}
diff --git a/pkg/event/fake/mock_event.go b/pkg/event/fake/mock_event.go
new file mode 100644
index 00000000..dcfc1d8b
--- /dev/null
+++ b/pkg/event/fake/mock_event.go
@@ -0,0 +1,85 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/vmware-tanzu/octant/pkg/event (interfaces: WSClientGetter,WSEventSender)
+
+// Package fake is a generated GoMock package.
+package fake
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+
+	event "github.com/vmware-tanzu/octant/pkg/event"
+)
+
+// MockWSClientGetter is a mock of WSClientGetter interface
+type MockWSClientGetter struct {
+	ctrl     *gomock.Controller
+	recorder *MockWSClientGetterMockRecorder
+}
+
+// MockWSClientGetterMockRecorder is the mock recorder for MockWSClientGetter
+type MockWSClientGetterMockRecorder struct {
+	mock *MockWSClientGetter
+}
+
+// NewMockWSClientGetter creates a new mock instance
+func NewMockWSClientGetter(ctrl *gomock.Controller) *MockWSClientGetter {
+	mock := &MockWSClientGetter{ctrl: ctrl}
+	mock.recorder = &MockWSClientGetterMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockWSClientGetter) EXPECT() *MockWSClientGetterMockRecorder {
+	return m.recorder
+}
+
+// Get mocks base method
+func (m *MockWSClientGetter) Get(arg0 string) event.WSEventSender {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get", arg0)
+	ret0, _ := ret[0].(event.WSEventSender)
+	return ret0
+}
+
+// Get indicates an expected call of Get
+func (mr *MockWSClientGetterMockRecorder) Get(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockWSClientGetter)(nil).Get), arg0)
+}
+
+// MockWSEventSender is a mock of WSEventSender interface
+type MockWSEventSender struct {
+	ctrl     *gomock.Controller
+	recorder *MockWSEventSenderMockRecorder
+}
+
+// MockWSEventSenderMockRecorder is the mock recorder for MockWSEventSender
+type MockWSEventSenderMockRecorder struct {
+	mock *MockWSEventSender
+}
+
+// NewMockWSEventSender creates a new mock instance
+func NewMockWSEventSender(ctrl *gomock.Controller) *MockWSEventSender {
+	mock := &MockWSEventSender{ctrl: ctrl}
+	mock.recorder = &MockWSEventSenderMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockWSEventSender) EXPECT() *MockWSEventSenderMockRecorder {
+	return m.recorder
+}
+
+// Send mocks base method
+func (m *MockWSEventSender) Send(arg0 event.Event) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Send", arg0)
+}
+
+// Send indicates an expected call of Send
+func (mr *MockWSEventSenderMockRecorder) Send(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockWSEventSender)(nil).Send), arg0)
+}
diff --git a/pkg/plugin/api/server.go b/pkg/plugin/api/server.go
index 563ecdcf..d68c0b32 100644
--- a/pkg/plugin/api/server.go
+++ b/pkg/plugin/api/server.go
@@ -79,19 +79,11 @@ type GRPCService struct {
 	PortForwarder          portforward.PortForwarder
 	FrontendProxy          FrontendProxy
 	NamespaceInterface     cluster.NamespaceInterface
-	WebsocketClientManager WSClientGetter
+	WebsocketClientManager event.WSClientGetter
 }
 
 var _ Service = (*GRPCService)(nil)
 
-type WSClientGetter interface {
-	Get(id string) WSEventSender
-}
-
-type WSEventSender interface {
-	Send(event event.Event)
-}
-
 // List lists objects.
 func (s *GRPCService) List(ctx context.Context, key store.Key) (*unstructured.UnstructuredList, error) {
 	// TODO: support hasSynced
diff --git a/pkg/plugin/javascript.go b/pkg/plugin/javascript.go
index 469603b7..1998f8ae 100644
--- a/pkg/plugin/javascript.go
+++ b/pkg/plugin/javascript.go
@@ -13,6 +13,8 @@ import (
 	"path"
 	"sync"
 
+	ocontext "github.com/vmware-tanzu/octant/internal/context"
+
 	"github.com/dop251/goja"
 	"github.com/dop251/goja_nodejs/eventloop"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -226,7 +228,7 @@ func (t *jsPlugin) Navigation(_ context.Context) (navigation.Navigation, error)
 }
 
 // Content returns the content response for a JavaScript plugin acting as a module.
-func (t *jsPlugin) Content(_ context.Context, contentPath string) (component.ContentResponse, error) {
+func (t *jsPlugin) Content(ctx context.Context, contentPath string) (component.ContentResponse, error) {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
@@ -234,6 +236,8 @@ func (t *jsPlugin) Content(_ context.Context, contentPath string) (component.Con
 	errCh := make(chan error)
 
 	t.loop.RunOnLoop(func(vm *goja.Runtime) {
+		clientID := ocontext.WebsocketClientIDFrom(ctx)
+
 		handler, err := vm.RunString("_concretePlugin.contentHandler")
 		if err != nil {
 			errCh <- fmt.Errorf("unable to load contentHandler from plugin: %w", err)
@@ -250,6 +254,9 @@ func (t *jsPlugin) Content(_ context.Context, contentPath string) (component.Con
 			errCh <- fmt.Errorf("unable to set contentPath: %w", err)
 			return
 		}
+		if err := obj.Set("clientID", vm.ToValue(clientID)); err != nil {
+			errCh <- fmt.Errorf("unable to set clientID: %w", err)
+		}
 		s, err := cHandler(t.pluginClass, obj)
 		if err != nil {
 			errCh <- fmt.Errorf("calling contentHandler: %w", err)
@@ -354,11 +361,11 @@ func (t *jsPlugin) Register(_ context.Context, _ string) (Metadata, error) {
 }
 
 // PrintTab returns the tab response from a JavaScript plugins tab handler.
-func (t *jsPlugin) PrintTab(_ context.Context, object runtime.Object) (TabResponse, error) {
+func (t *jsPlugin) PrintTab(ctx context.Context, object runtime.Object) (TabResponse, error) {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
-	tabResponse, err := t.objectRequestCall("tabHandler", object)
+	tabResponse, err := t.objectRequestCall(ctx, "tabHandler", object)
 	if err != nil {
 		return TabResponse{}, err
 	}
@@ -393,11 +400,11 @@ func (t *jsPlugin) PrintTab(_ context.Context, object runtime.Object) (TabRespon
 }
 
 // ObjectStats returns the object status from a JavaScript plugins object status handler.
-func (t *jsPlugin) ObjectStatus(_ context.Context, object runtime.Object) (ObjectStatusResponse, error) {
+func (t *jsPlugin) ObjectStatus(ctx context.Context, object runtime.Object) (ObjectStatusResponse, error) {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
-	osResponse, err := t.objectRequestCall("objectStatusHandler", object)
+	osResponse, err := t.objectRequestCall(ctx, "objectStatusHandler", object)
 	if err != nil {
 		return ObjectStatusResponse{}, err
 	}
@@ -428,13 +435,15 @@ func (t *jsPlugin) ObjectStatus(_ context.Context, object runtime.Object) (Objec
 }
 
 // HandleAction calls the JavaScript plugins action handler.
-func (t *jsPlugin) HandleAction(_ context.Context, actionPath string, payload action.Payload) error {
+func (t *jsPlugin) HandleAction(ctx context.Context, actionPath string, payload action.Payload) error {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
 	errCh := make(chan error)
 
 	t.loop.RunOnLoop(func(vm *goja.Runtime) {
+		clientID := ocontext.WebsocketClientIDFrom(ctx)
+
 		handler, err := vm.RunString("_concretePlugin.actionHandler")
 		if err != nil {
 			errCh <- fmt.Errorf("unable to load actionHandler from plugin: %w", err)
@@ -459,6 +468,10 @@ func (t *jsPlugin) HandleAction(_ context.Context, actionPath string, payload ac
 			errCh <- fmt.Errorf("unable to set payload: %w", err)
 			return
 		}
+		if err := obj.Set("clientID", clientID); err != nil {
+			errCh <- fmt.Errorf("unable to set clientID: %w", err)
+			return
+		}
 
 		s, err := cHandler(t.pluginClass, obj)
 		if err != nil {
@@ -486,11 +499,11 @@ func (t *jsPlugin) HandleAction(_ context.Context, actionPath string, payload ac
 }
 
 // Print returns the print response from the JavaScript plugins print handler.
-func (t *jsPlugin) Print(_ context.Context, object runtime.Object) (PrintResponse, error) {
+func (t *jsPlugin) Print(ctx context.Context, object runtime.Object) (PrintResponse, error) {
 	t.mu.Lock()
 	defer t.mu.Unlock()
 
-	printResponse, err := t.objectRequestCall("printHandler", object)
+	printResponse, err := t.objectRequestCall(ctx, "printHandler", object)
 	if err != nil {
 		return PrintResponse{}, err
 	}
@@ -537,11 +550,13 @@ func (t *jsPlugin) Print(_ context.Context, object runtime.Object) (PrintRespons
 	return response, nil
 }
 
-func (t *jsPlugin) objectRequestCall(handlerName string, object runtime.Object) (*goja.Object, error) {
+func (t *jsPlugin) objectRequestCall(ctx context.Context, handlerName string, object runtime.Object) (*goja.Object, error) {
 	errCh := make(chan error)
 	var response *goja.Object
 
 	t.loop.RunOnLoop(func(vm *goja.Runtime) {
+		clientID := ocontext.WebsocketClientIDFrom(ctx)
+
 		handler, err := vm.RunString(fmt.Sprintf("_concretePlugin.%s", handlerName))
 		if err != nil {
 			errCh <- fmt.Errorf("unable to load %s from plugin: %w", handlerName, err)
@@ -559,6 +574,10 @@ func (t *jsPlugin) objectRequestCall(handlerName string, object runtime.Object)
 			errCh <- fmt.Errorf("unable to set object: %w", err)
 			return
 		}
+		if err := obj.Set("clientID", vm.ToValue(clientID)); err != nil {
+			errCh <- fmt.Errorf("unable to set clientID: %w", err)
+			return
+		}
 		s, err := cHandler(t.pluginClass, obj)
 		if err != nil {
 			errCh <- err
diff --git a/pkg/plugin/javascript/dashboard_client.go b/pkg/plugin/javascript/dashboard_client.go
index 3bd5ff9e..07030bf5 100644
--- a/pkg/plugin/javascript/dashboard_client.go
+++ b/pkg/plugin/javascript/dashboard_client.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/vmware-tanzu/octant/pkg/event"
+
 	"github.com/dop251/goja"
 
 	"github.com/vmware-tanzu/octant/internal/octant"
@@ -50,13 +52,14 @@ type OctantClient interface {
 }
 
 // DefaultFunctions are the default functions for the ModularDashboardClientFactory.
-func DefaultFunctions(octantClient OctantClient) []octant.DashboardClientFunction {
+func DefaultFunctions(octantClient OctantClient, wsClient event.WSClientGetter) []octant.DashboardClientFunction {
 	return []octant.DashboardClientFunction{
 		NewDashboardGet(octantClient),
 		NewDashboardList(octantClient),
 		NewDashboardUpdate(octantClient),
 		NewDashboardDelete(octantClient),
 		NewDashboardRefPath(octantClient),
+		NewDashboardSendEvent(wsClient),
 	}
 }
 
diff --git a/pkg/plugin/javascript/dashboard_send_event.go b/pkg/plugin/javascript/dashboard_send_event.go
new file mode 100644
index 00000000..b5b787ca
--- /dev/null
+++ b/pkg/plugin/javascript/dashboard_send_event.go
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package javascript
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/dop251/goja"
+
+	ocontext "github.com/vmware-tanzu/octant/internal/context"
+	"github.com/vmware-tanzu/octant/internal/octant"
+	"github.com/vmware-tanzu/octant/pkg/action"
+	"github.com/vmware-tanzu/octant/pkg/event"
+)
+
+// DashboardList is a function that lists objects by key.
+type DashboardSendEvent struct {
+	WebsocketClientManager event.WSClientGetter
+}
+
+var _ octant.DashboardClientFunction = &DashboardSendEvent{}
+
+// NewDashboardSendEvent creates an instance of DashboardSendEvent.
+func NewDashboardSendEvent(websocketClientManager event.WSClientGetter) *DashboardSendEvent {
+	d := &DashboardSendEvent{
+		WebsocketClientManager: websocketClientManager,
+	}
+	return d
+}
+
+// Name returns the name of this function. It will always return "SendEvent".
+func (d *DashboardSendEvent) Name() string {
+	return "SendEvent"
+}
+
+// Call creates a function call that sends an event to a websocket client. If no ClientID
+// is provided or the clientID cannot be found a javascript exception is raised.
+func (d *DashboardSendEvent) Call(ctx context.Context, vm *goja.Runtime) func(c goja.FunctionCall) goja.Value {
+	// clientID string, event EventType, payload action.Payload
+	return func(c goja.FunctionCall) goja.Value {
+		clientID := c.Argument(0).String()
+		if clientID == "" {
+			panic(panicMessage(vm, fmt.Errorf("clientID is empty"), ""))
+		}
+
+		eventType := event.EventType(c.Argument(1).String())
+		if eventType == "" {
+			panic(panicMessage(vm, fmt.Errorf("eventType is empty"), ""))
+		}
+
+		var payload action.Payload
+		obj := c.Argument(2).ToObject(vm)
+
+		// This will never error since &key is a pointer to a type.
+		_ = vm.ExportTo(obj, &payload)
+
+		event := event.CreateEvent(eventType, payload)
+
+		if d.WebsocketClientManager == nil {
+			panic(panicMessage(vm, fmt.Errorf("websocket client manager is nil"), ""))
+		}
+
+		sender := d.WebsocketClientManager.Get(clientID)
+		if sender == nil {
+			panic(panicMessage(vm, fmt.Errorf("unable to find ws client %s", ocontext.WebsocketClientIDFrom(ctx)), ""))
+		}
+
+		sender.Send(event)
+
+		return goja.Undefined()
+	}
+}
diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go
index 502c6de4..213aa3bc 100644
--- a/pkg/plugin/manager.go
+++ b/pkg/plugin/manager.go
@@ -18,6 +18,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/vmware-tanzu/octant/pkg/event"
+
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/dynamic"
 
@@ -269,6 +271,7 @@ type Manager struct {
 	ClientFactory   ClientFactory
 	ModuleRegistrar ModuleRegistrar
 	ActionRegistrar ActionRegistrar
+	WSClient        event.WSClientGetter
 
 	Runners Runners
 
@@ -282,7 +285,7 @@ type Manager struct {
 var _ ManagerInterface = (*Manager)(nil)
 
 // NewManager creates an instance of Manager.
-func NewManager(apiService api.API, moduleRegistrar ModuleRegistrar, actionRegistrar ActionRegistrar, options ...ManagerOption) *Manager {
+func NewManager(apiService api.API, moduleRegistrar ModuleRegistrar, actionRegistrar ActionRegistrar, ws event.WSClientGetter, options ...ManagerOption) *Manager {
 	m := &Manager{
 		store:           NewDefaultStore(),
 		ClientFactory:   NewDefaultClientFactory(),
@@ -290,6 +293,7 @@ func NewManager(apiService api.API, moduleRegistrar ModuleRegistrar, actionRegis
 		API:             apiService,
 		ModuleRegistrar: moduleRegistrar,
 		ActionRegistrar: actionRegistrar,
+		WSClient:        ws,
 	}
 
 	for _, option := range options {
@@ -445,7 +449,7 @@ func (m *Manager) unregisterJSPlugin(_ context.Context, p JSPlugin) error {
 }
 
 func (m *Manager) registerJSPlugin(ctx context.Context, pluginPath string) error {
-	dashboardClientFactory := javascript.NewModularDashboardClientFactory(javascript.DefaultFunctions(m.octantClient))
+	dashboardClientFactory := javascript.NewModularDashboardClientFactory(javascript.DefaultFunctions(m.octantClient, m.WSClient))
 
 	jsPlugin, err := NewJSPlugin(ctx, pluginPath, dashboardClientFactory)
 	if err != nil {
diff --git a/web/package-lock.json b/web/package-lock.json
index c5d85569..f225ce4e 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1253,7 +1253,7 @@
         },
         "source-map": {
           "version": "0.5.7",
-          "resolved": "https://build-artifactory.eng.vmware.com/api/npm/npm/source-map/-/source-map-0.5.7.tgz",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
           "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
           "dev": true
         }
@@ -13339,14 +13339,14 @@
     },
     "character-entities": {
       "version": "1.2.4",
-      "resolved": "http://build-artifactory.eng.vmware.com/api/npm/npm/character-entities/-/character-entities-1.2.4.tgz",
-      "integrity": "sha1-4Sw5Obfq9OWxXnrUxeKOHUjFsWs=",
+      "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
+      "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
       "dev": true
     },
     "character-entities-legacy": {
       "version": "1.1.4",
-      "resolved": "http://build-artifactory.eng.vmware.com/api/npm/npm/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
-      "integrity": "sha1-lLwYRdznClu50uzHSHJWYSk9j8E=",
+      "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
+      "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
       "dev": true
     },
     "character-reference-invalid": {
@@ -15227,6 +15227,11 @@
         "ip-regex": "^2.1.0"
       }
     },
+    "default-passive-events": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/default-passive-events/-/default-passive-events-1.0.10.tgz",
+      "integrity": "sha1-KK0yaWSKdqAVj0E9ZuN68k2tBTo="
+    },
     "defaults": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@@ -20255,8 +20260,8 @@
     },
     "latest-version": {
       "version": "5.1.0",
-      "resolved": "http://build-artifactory.eng.vmware.com/api/npm/npm/latest-version/-/latest-version-5.1.0.tgz",
-      "integrity": "sha1-EZ3+kI/jjRXfpD7NE/oS7Igy+s4=",
+      "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
+      "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
       "dev": true,
       "requires": {
         "package-json": "^6.3.0"
diff --git a/web/package.json b/web/package.json
index be058a0a..174b8d69 100644
--- a/web/package.json
+++ b/web/package.json
@@ -59,12 +59,13 @@
     "d3-graphviz": "^2.6.1",
     "d3-zoom": "^1.8.3",
     "dagre-d3": "^0.6.4",
+    "default-passive-events": "^1.0.10",
     "highlight.js": "^10.1.1",
     "lodash": "^4.17.15",
     "lodash-es": "^4.17.15",
     "monaco-editor": "^0.20.0",
     "ng-monaco-editor": "^2.2.2",
-    "ngx-highlightjs": "^4.1.1",
+    "ngx-highlightjs": "^4.0.2",
     "ngx-markdown": "^8.2.1",
     "rxjs": "^6.6.0",
     "serialize-javascript": "^3.0.0",
diff --git a/web/src/app/app.module.ts b/web/src/app/app.module.ts
index a68bde30..b70c42d6 100644
--- a/web/src/app/app.module.ts
+++ b/web/src/app/app.module.ts
@@ -1,7 +1,7 @@
 // Copyright (c) 2019 the Octant contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 //
-import { CommonModule, Location } from '@angular/common';
+import { Location } from '@angular/common';
 import { HttpClientModule } from '@angular/common/http';
 import { Injectable, NgModule } from '@angular/core';
 import { RouteReuseStrategy, RouterModule } from '@angular/router';
@@ -10,7 +10,7 @@ import { AppRoutingModule } from './app-routing.module';
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { highlightProvider } from './modules/shared/highlight';
-import { MonacoEditorModule } from 'ng-monaco-editor';
+import { MonacoEditorModule, MonacoProviderService } from 'ng-monaco-editor';
 import { ComponentReuseStrategy } from './modules/shared/component-reuse.strategy';
 
 @Injectable()
@@ -23,7 +23,6 @@ export class UnstripTrailingSlashLocation extends Location {
 @NgModule({
   declarations: [HomeComponent],
   imports: [
-    CommonModule,
     BrowserModule,
     BrowserAnimationsModule,
     HttpClientModule,
diff --git a/web/src/app/modules/shared/components/abstract-view/abstract-view.component.ts b/web/src/app/modules/shared/components/abstract-view/abstract-view.component.ts
deleted file mode 100644
index 1cf41bd4..00000000
--- a/web/src/app/modules/shared/components/abstract-view/abstract-view.component.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import {
-  AfterViewInit,
-  EventEmitter,
-  Input,
-  OnInit,
-  Output,
-} from '@angular/core';
-import { View } from '../../models/content';
-
-export abstract class AbstractViewComponent<T>
-  implements OnInit, AfterViewInit {
-  v: T;
-
-  @Input() set view(v: View) {
-    this.v = (v as unknown) as T;
-    this.update();
-  }
-
-  get view() {
-    return (this.v as unknown) as View;
-  }
-
-  @Output() viewInit: EventEmitter<void> = new EventEmitter<void>();
-
-  private hasChildren = false;
-  private initialChildCountActual = 0;
-
-  protected set initialChildCount(n: number) {
-    this.waitCountActual = n;
-    if (n > 0) {
-      this.hasChildren = true;
-      this.initialChildCountActual = n;
-    }
-  }
-
-  private waitCountActual = 0;
-  protected set waitCount(n: number) {
-    this.waitCountActual = n;
-    if (n === 0 && this.hasChildren) {
-      this.viewInit.emit();
-      return;
-    }
-  }
-
-  protected get waitCount(): number {
-    return this.waitCountActual;
-  }
-
-  protected abstract update(): void;
-
-  ngOnInit(): void {}
-
-  ngAfterViewInit() {
-    if (this.initialChildCountActual === 0) {
-      this.viewInit.emit();
-    }
-  }
-
-  ping() {
-    this.update();
-  }
-
-  initDone() {
-    this.waitCount = this.waitCount - 1;
-  }
-}
diff --git a/web/src/app/modules/shared/components/missing-component/missing-component.component.html b/web/src/app/modules/shared/components/missing-component/missing-component.component.html
deleted file mode 100644
index e0da4c47..00000000
--- a/web/src/app/modules/shared/components/missing-component/missing-component.component.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<div class="alert alert-danger static">
-  <span class="alert-icon-wrapper">
-    <clr-icon class="alert-icon" shape="exclamation-circle"></clr-icon>
-  </span>
-  <span class="alert-text"> {{ name }} not implemented </span>
-</div>
diff --git a/web/src/app/modules/shared/components/missing-component/missing-component.component.sass b/web/src/app/modules/shared/components/missing-component/missing-component.component.sass
deleted file mode 100644
index e69de29b..00000000
diff --git a/web/src/app/modules/shared/components/missing-component/missing-component.component.spec.ts b/web/src/app/modules/shared/components/missing-component/missing-component.component.spec.ts
deleted file mode 100644
index 09443750..00000000
--- a/web/src/app/modules/shared/components/missing-component/missing-component.component.spec.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { MissingComponentComponent } from './missing-component.component';
-import { BrowserModule } from '@angular/platform-browser';
-import { CommonModule } from '@angular/common';
-
-describe('MissingComponentComponent', () => {
-  let component: MissingComponentComponent;
-  let fixture: ComponentFixture<MissingComponentComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [MissingComponentComponent],
-      imports: [CommonModule],
-    }).compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(MissingComponentComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/web/src/app/modules/shared/components/missing-component/missing-component.component.ts b/web/src/app/modules/shared/components/missing-component/missing-component.component.ts
deleted file mode 100644
index 3978d54a..00000000
--- a/web/src/app/modules/shared/components/missing-component/missing-component.component.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Component, Input } from '@angular/core';
-
-@Component({
-  selector: 'app-missing-component',
-  templateUrl: './missing-component.component.html',
-  styleUrls: ['./missing-component.component.sass'],
-})
-export class MissingComponentComponent {
-  @Input() name: string;
-
-  constructor() {}
-}
diff --git a/web/src/app/modules/shared/components/presentation/annotations/annotations.component.ts b/web/src/app/modules/shared/components/presentation/annotations/annotations.component.ts
index c8bd36b9..fcaed6d5 100644
--- a/web/src/app/modules/shared/components/presentation/annotations/annotations.component.ts
+++ b/web/src/app/modules/shared/components/presentation/annotations/annotations.component.ts
@@ -2,30 +2,39 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
-import { AnnotationsView } from 'src/app/modules/shared/models/content';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { AnnotationsView, View } from 'src/app/modules/shared/models/content';
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-annotations',
   templateUrl: './annotations.component.html',
   styleUrls: ['./annotations.component.scss'],
 })
-export class AnnotationsComponent extends AbstractViewComponent<
-  AnnotationsView
-> {
+export class AnnotationsComponent implements OnChanges {
+  private v: AnnotationsView;
+
+  @Input() set view(v: View) {
+    this.v = v as AnnotationsView;
+  }
+
+  get view() {
+    return this.v;
+  }
+
   annotations: { [key: string]: string };
   annotationKeys: string[];
   trackByIdentity = trackByIdentity;
 
-  constructor() {
-    super();
-  }
+  constructor() {}
 
-  update() {
-    const view = this.v;
-    this.annotations = view.config.annotations;
-    this.annotationKeys = this.annotations ? Object.keys(this.annotations) : [];
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as AnnotationsView;
+      this.annotations = view.config.annotations;
+      this.annotationKeys = this.annotations
+        ? Object.keys(this.annotations)
+        : [];
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/breadcrumb/breadcrumb.component.scss b/web/src/app/modules/shared/components/presentation/breadcrumb/breadcrumb.component.scss
index d901b855..a64754a4 100644
--- a/web/src/app/modules/shared/components/presentation/breadcrumb/breadcrumb.component.scss
+++ b/web/src/app/modules/shared/components/presentation/breadcrumb/breadcrumb.component.scss
@@ -7,7 +7,7 @@
 }
 
 :host-context(body.dark) {
-  --creadcrumb-link-color: #49afd9;
+  --creadcrumb-link-color: #49AFD9;
 }
 
 .breadcrumb {
diff --git a/web/src/app/modules/shared/components/presentation/button-group/button-group.component.html b/web/src/app/modules/shared/components/presentation/button-group/button-group.component.html
index bc33678c..fef6cfff 100644
--- a/web/src/app/modules/shared/components/presentation/button-group/button-group.component.html
+++ b/web/src/app/modules/shared/components/presentation/button-group/button-group.component.html
@@ -1,6 +1,6 @@
-<clr-button-group *ngIf="v.config.buttons" class="btn-primary">
+<clr-button-group *ngIf="view?.config.buttons" class="btn-primary">
   <clr-button
-    *ngFor="let button of v.config.buttons; trackBy: trackByFn"
+    *ngFor="let button of view.config.buttons; trackBy: trackByFn"
     class="{{ class }}"
     (click)="onClick(button.payload, button.confirmation)"
   >
diff --git a/web/src/app/modules/shared/components/presentation/button-group/button-group.component.spec.ts b/web/src/app/modules/shared/components/presentation/button-group/button-group.component.spec.ts
index e442cfe9..db358cea 100644
--- a/web/src/app/modules/shared/components/presentation/button-group/button-group.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/button-group/button-group.component.spec.ts
@@ -1,7 +1,6 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { ButtonGroupComponent } from './button-group.component';
-import { ButtonGroupView } from '../../../models/content';
 
 describe('ButtonGroupComponent', () => {
   let component: ButtonGroupComponent;
@@ -16,13 +15,6 @@ describe('ButtonGroupComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(ButtonGroupComponent);
     component = fixture.componentInstance;
-
-    component.view = {
-      config: {
-        buttons: [],
-      },
-    } as ButtonGroupView;
-
     fixture.detectChanges();
   });
 
diff --git a/web/src/app/modules/shared/components/presentation/button-group/button-group.component.ts b/web/src/app/modules/shared/components/presentation/button-group/button-group.component.ts
index da63978e..933d5355 100644
--- a/web/src/app/modules/shared/components/presentation/button-group/button-group.component.ts
+++ b/web/src/app/modules/shared/components/presentation/button-group/button-group.component.ts
@@ -1,16 +1,22 @@
-import { Component, EventEmitter, Output } from '@angular/core';
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import { ButtonGroupView, Confirmation } from '../../../models/content';
 import { ActionService } from '../../../services/action/action.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-button-group',
   templateUrl: './button-group.component.html',
   styleUrls: ['./button-group.component.scss'],
 })
-export class ButtonGroupComponent extends AbstractViewComponent<
-  ButtonGroupView
-> {
+export class ButtonGroupComponent implements OnInit {
+  v: ButtonGroupView;
+
+  @Input() set view(v: ButtonGroupView) {
+    this.v = v as ButtonGroupView;
+  }
+  get view() {
+    return this.v;
+  }
+
   @Output() buttonLoad: EventEmitter<boolean> = new EventEmitter(true);
 
   isModalOpen = false;
@@ -19,13 +25,11 @@ export class ButtonGroupComponent extends AbstractViewComponent<
   payload = {};
   class = '';
 
-  constructor(private actionService: ActionService) {
-    super();
-  }
+  constructor(private actionService: ActionService) {}
 
-  update() {
-    if (this.v.config.buttons) {
-      this.v.config.buttons.forEach(button => {
+  ngOnInit() {
+    if (this.view && this.view.config.buttons) {
+      this.view.config.buttons.forEach(button => {
         if (button.confirmation) {
           this.class = 'btn-danger-outline btn-sm';
         } else {
diff --git a/web/src/app/modules/shared/components/presentation/card-list/card-list.component.ts b/web/src/app/modules/shared/components/presentation/card-list/card-list.component.ts
index 3aef4089..26e4eb19 100644
--- a/web/src/app/modules/shared/components/presentation/card-list/card-list.component.ts
+++ b/web/src/app/modules/shared/components/presentation/card-list/card-list.component.ts
@@ -1,19 +1,23 @@
-import { Component } from '@angular/core';
-import { CardListView, CardView } from '../../../models/content';
+import { Component, Input } from '@angular/core';
+import { CardListView, CardView, View } from '../../../models/content';
 import { ViewService } from '../../../services/view/view.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-card-list',
   templateUrl: './card-list.component.html',
   styleUrls: ['./card-list.component.scss'],
 })
-export class CardListComponent extends AbstractViewComponent<CardListView> {
-  constructor(private viewService: ViewService) {
-    super();
+export class CardListComponent {
+  v: CardListView;
+
+  @Input() set view(v: View) {
+    this.v = v as CardListView;
+  }
+  get view() {
+    return this.v;
   }
 
-  update() {}
+  constructor(private viewService: ViewService) {}
 
   identifyCard = (index: number, item: CardView): string => {
     return [index, this.viewService.viewTitleAsText(item)].join(',');
diff --git a/web/src/app/modules/shared/components/presentation/card/card.component.html b/web/src/app/modules/shared/components/presentation/card/card.component.html
index 07a1406e..9921edd2 100644
--- a/web/src/app/modules/shared/components/presentation/card/card.component.html
+++ b/web/src/app/modules/shared/components/presentation/card/card.component.html
@@ -16,11 +16,11 @@
 <ng-template #content>
   <div class="card">
     <div class="card-block">
-      <ng-container *ngIf="v.config.alert">
-        <div class="alert alert-{{ v.config.alert.type }} alert-sm">
+      <ng-container *ngIf="view.config.alert">
+        <div class="alert alert-{{ view.config.alert.type }} alert-sm">
           <div class="alert-item static">
             <span class="alert-text">
-              {{ v.config.alert.message }}
+              {{ view.config.alert.message }}
             </span>
           </div>
         </div>
@@ -29,13 +29,15 @@
         <app-view-title [views]="title"></app-view-title>
       </h4>
 
-      <div class="card-text">
-        <app-view-container [view]="body"></app-view-container>
+      <div [ngSwitch]="body?.metadata.type" class="card-text">
+        <app-content-switcher [view]="body"></app-content-switcher>
       </div>
     </div>
 
-    <div class="card-footer" *ngIf="v.config.actions?.length > 0">
-      <ng-container *ngFor="let action of v.config.actions; trackBy: trackByFn">
+    <div class="card-footer" *ngIf="view.config.actions?.length > 0">
+      <ng-container
+        *ngFor="let action of view.config.actions; trackBy: trackByFn"
+      >
         <button class="btn btn-sm btn-link" (click)="setAction(action)">
           {{ action.name }}
         </button>
diff --git a/web/src/app/modules/shared/components/presentation/card/card.component.spec.ts b/web/src/app/modules/shared/components/presentation/card/card.component.spec.ts
index 16021f17..9b6e0afb 100644
--- a/web/src/app/modules/shared/components/presentation/card/card.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/card/card.component.spec.ts
@@ -8,7 +8,8 @@ import {
 
 import { CardComponent } from './card.component';
 import { Action, CardView, TextView } from '../../../models/content';
-import { FormBuilder, FormGroup } from '@angular/forms';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { SimpleChange } from '@angular/core';
 import { ViewService } from '../../../services/view/view.service';
 import { viewServiceStub } from 'src/app/testing/view-service.stub';
 import { SharedModule } from '../../../shared.module';
@@ -51,16 +52,15 @@ describe('CardComponent', () => {
       },
     };
 
-    component.view = {
+    const cardView: CardView = {
       config: {
         actions: [],
         body: textView,
       },
-      metadata: {
-        type: 'card',
-        title: [textView],
-      },
-    } as CardView;
+      metadata: undefined,
+    };
+
+    component.view = cardView;
     component.currentAction = action;
   });
 
@@ -110,6 +110,10 @@ describe('CardComponent', () => {
     };
 
     component.view = view;
+
+    component.ngOnChanges({
+      view: new SimpleChange(null, component.view, false),
+    });
     fixture.detectChanges();
 
     expect(component.body).toBe(view.config.body);
diff --git a/web/src/app/modules/shared/components/presentation/card/card.component.ts b/web/src/app/modules/shared/components/presentation/card/card.component.ts
index 6bd22a01..01c4acf8 100644
--- a/web/src/app/modules/shared/components/presentation/card/card.component.ts
+++ b/web/src/app/modules/shared/components/presentation/card/card.component.ts
@@ -1,16 +1,31 @@
-import { Component, Input, ViewChild } from '@angular/core';
+import {
+  Component,
+  Input,
+  OnChanges,
+  SimpleChanges,
+  ViewChild,
+} from '@angular/core';
 import { Action, CardView, TitleView, View } from '../../../models/content';
 import { FormGroup } from '@angular/forms';
 import { ActionService } from '../../../services/action/action.service';
 import { FormComponent } from '../form/form.component';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-card',
   templateUrl: './card.component.html',
   styleUrls: ['./card.component.scss'],
 })
-export class CardComponent extends AbstractViewComponent<CardView> {
+export class CardComponent implements OnChanges {
+  private v: CardView;
+
+  @Input() set view(v: View) {
+    this.v = v as CardView;
+  }
+
+  get view() {
+    return this.v;
+  }
+
   @ViewChild('appForm') appForm: FormComponent;
 
   title: TitleView[];
@@ -19,13 +34,16 @@ export class CardComponent extends AbstractViewComponent<CardView> {
 
   currentAction: Action;
 
-  constructor(private actionService: ActionService) {
-    super();
-  }
+  constructor(private actionService: ActionService) {}
 
-  update() {
-    this.title = this.v.metadata.title as TitleView[];
-    this.body = this.v.config.body;
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view: CardView = changes.view.currentValue;
+      if (view) {
+        this.title = view.metadata.title as TitleView[];
+        this.body = view.config.body;
+      }
+    }
   }
 
   onActionSubmit(formGroup: FormGroup) {
diff --git a/web/src/app/modules/shared/components/presentation/code/code.component.ts b/web/src/app/modules/shared/components/presentation/code/code.component.ts
index 8c0da091..8f344863 100644
--- a/web/src/app/modules/shared/components/presentation/code/code.component.ts
+++ b/web/src/app/modules/shared/components/presentation/code/code.component.ts
@@ -3,24 +3,34 @@ Copyright (c) 2020 the Octant contributors. All Rights Reserved.
 SPDX-License-Identifier: Apache-2.0
 */
 
-import { Component } from '@angular/core';
-import { CodeView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { CodeView, View } from 'src/app/modules/shared/models/content';
 
 @Component({
   selector: 'app-view-code',
   templateUrl: './code.component.html',
   styleUrls: ['./code.component.scss'],
 })
-export class CodeComponent extends AbstractViewComponent<CodeView> {
-  value: string;
+export class CodeComponent implements OnChanges {
+  private v: CodeView;
+
+  @Input() set view(v: View) {
+    this.v = v as CodeView;
+  }
 
-  constructor() {
-    super();
+  get view() {
+    return this.v;
   }
 
-  update() {
-    this.value = this.v.config.value;
+  value: string;
+
+  constructor() {}
+
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as CodeView;
+      this.value = view.config.value;
+    }
   }
 
   copyToClipboard(text) {
diff --git a/web/src/app/modules/shared/components/presentation/containers/containers.component.ts b/web/src/app/modules/shared/components/presentation/containers/containers.component.ts
index 096d28d2..14edf743 100644
--- a/web/src/app/modules/shared/components/presentation/containers/containers.component.ts
+++ b/web/src/app/modules/shared/components/presentation/containers/containers.component.ts
@@ -2,27 +2,37 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import {
   ContainerDef,
   ContainersView,
+  View,
 } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-containers',
   templateUrl: './containers.component.html',
   styleUrls: ['./containers.component.scss'],
 })
-export class ContainersComponent extends AbstractViewComponent<ContainersView> {
-  containers: ContainerDef[];
+export class ContainersComponent implements OnChanges {
+  private v: ContainersView;
 
-  constructor() {
-    super();
+  @Input() set view(v: View) {
+    this.v = v as ContainersView;
+  }
+  get view() {
+    return this.v;
   }
 
-  update() {
-    this.containers = this.v.config.containers;
+  containers: ContainerDef[];
+
+  constructor() {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as ContainersView;
+      this.containers = view.config.containers;
+    }
   }
 
   trackItem(index: number, item: ContainerDef): string {
diff --git a/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.html b/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.html
new file mode 100644
index 00000000..c9554baa
--- /dev/null
+++ b/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.html
@@ -0,0 +1,111 @@
+<ng-container *ngIf="view?.metadata.type">
+  <ng-container [ngSwitch]="view.metadata?.type">
+    <ng-container *ngSwitchCase="'annotations'">
+      <app-view-annotations [view]="view"></app-view-annotations>
+    </ng-container>
+    <ng-container *ngSwitchCase="'buttonGroup'">
+      <app-button-group [view]="view"></app-button-group>
+    </ng-container>
+    <ng-container *ngSwitchCase="'card'">
+      <app-view-card [view]="view"></app-view-card>
+    </ng-container>
+    <ng-container *ngSwitchCase="'cardList'">
+      <app-view-card-list [view]="view"></app-view-card-list>
+    </ng-container>
+    <ng-container *ngSwitchCase="'containers'">
+      <app-view-containers [view]="view"></app-view-containers>
+    </ng-container>
+    <ng-container *ngSwitchCase="'codeBlock'">
+      <app-view-code [view]="view"></app-view-code>
+    </ng-container>
+    <ng-container *ngSwitchCase="'donutChart'">
+      <app-view-donut-chart [view]="view"></app-view-donut-chart>
+    </ng-container>
+    <ng-container *ngSwitchCase="'editor'">
+      <app-view-editor [view]="view"></app-view-editor>
+    </ng-container>
+    <ng-container *ngSwitchCase="'expressionSelector'">
+      <app-view-expression-selector
+        [view]="view"
+      ></app-view-expression-selector>
+    </ng-container>
+    <ng-container *ngSwitchCase="'graphviz'">
+      <app-view-graphviz [view]="view"></app-view-graphviz>
+    </ng-container>
+    <ng-container *ngSwitchCase="'flexlayout'">
+      <app-view-flexlayout [view]="view"></app-view-flexlayout>
+    </ng-container>
+    <ng-container *ngSwitchCase="'labels'">
+      <app-view-labels [view]="view"></app-view-labels>
+    </ng-container>
+    <ng-container *ngSwitchCase="'labelSelector'">
+      <app-view-label-selector [view]="view"></app-view-label-selector>
+    </ng-container>
+    <ng-container *ngSwitchCase="'loading'">
+      <app-view-loading [view]="view"></app-view-loading>
+    </ng-container>
+    <ng-container *ngSwitchCase="'error'">
+      <app-view-error [view]="view"></app-view-error>
+    </ng-container>
+    <ng-container *ngSwitchCase="'iframe'">
+      <app-iframe [view]="view"></app-iframe>
+    </ng-container>
+    <ng-container *ngSwitchCase="'link'">
+      <app-view-link [view]="view"></app-view-link>
+    </ng-container>
+    <ng-container *ngSwitchCase="'list'">
+      <app-view-list [view]="view"></app-view-list>
+    </ng-container>
+    <ng-container *ngSwitchCase="'logs'">
+      <app-logs [view]="view"></app-logs>
+    </ng-container>
+    <ng-container *ngSwitchCase="'podStatus'">
+      <app-pod-status [view]="view"></app-pod-status>
+    </ng-container>
+    <ng-container *ngSwitchCase="'portforward'">
+      <app-view-port-forward [view]="view"></app-view-port-forward>
+    </ng-container>
+    <ng-container *ngSwitchCase="'ports'">
+      <app-view-ports [view]="view"></app-view-ports>
+    </ng-container>
+    <ng-container *ngSwitchCase="'quadrant'">
+      <app-view-quadrant [view]="view"></app-view-quadrant>
+    </ng-container>
+    <ng-container *ngSwitchCase="'resourceViewer'">
+      <app-view-resource-viewer [view]="view"></app-view-resource-viewer>
+    </ng-container>
+    <ng-container *ngSwitchCase="'selectors'">
+      <app-view-selectors [view]="view"></app-view-selectors>
+    </ng-container>
+    <ng-container *ngSwitchCase="'singleStat'">
+      <app-single-stat [view]="view"></app-single-stat>
+    </ng-container>
+    <ng-container *ngSwitchCase="'summary'">
+      <app-view-summary [view]="view"></app-view-summary>
+    </ng-container>
+    <ng-container *ngSwitchCase="'table'">
+      <app-view-datagrid [view]="view"></app-view-datagrid>
+    </ng-container>
+    <ng-container *ngSwitchCase="'text'">
+      <app-view-text [view]="view"></app-view-text>
+    </ng-container>
+    <ng-container *ngSwitchCase="'timestamp'">
+      <app-view-timestamp [view]="view"></app-view-timestamp>
+    </ng-container>
+    <ng-container *ngSwitchCase="'yaml'">
+      <app-view-yaml [view]="view"></app-view-yaml>
+    </ng-container>
+    <ng-container *ngSwitchCase="'terminal'">
+      <app-terminal [view]="view"></app-terminal>
+    </ng-container>
+    <ng-container *ngSwitchCase="'stepper'">
+      <app-stepper [view]="view"></app-stepper>
+    </ng-container>
+    <div class="alert alert-danger static" *ngSwitchDefault>
+      <span class="alert-icon-wrapper">
+        <clr-icon class="alert-icon" shape="exclamation-circle"></clr-icon>
+      </span>
+      <span class="alert-text"> {{ view.metadata.type }} not implemented </span>
+    </div>
+  </ng-container>
+</ng-container>
diff --git a/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.scss b/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.scss
new file mode 100644
index 00000000..e78526e0
--- /dev/null
+++ b/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.scss
@@ -0,0 +1,3 @@
+/* Copyright (c) 2019 the Octant contributors. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
diff --git a/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.spec.ts b/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.spec.ts
new file mode 100644
index 00000000..92a737c0
--- /dev/null
+++ b/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.spec.ts
@@ -0,0 +1,29 @@
+// Copyright (c) 2019 the Octant contributors. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ContentSwitcherComponent } from './content-switcher.component';
+import { SharedModule } from '../../../shared.module';
+
+describe('ContentSwitcherComponent', () => {
+  let component: ContentSwitcherComponent;
+  let fixture: ComponentFixture<ContentSwitcherComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [SharedModule],
+    }).compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ContentSwitcherComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.ts b/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.ts
new file mode 100644
index 00000000..19b1be6f
--- /dev/null
+++ b/web/src/app/modules/shared/components/presentation/content-switcher/content-switcher.component.ts
@@ -0,0 +1,19 @@
+// Copyright (c) 2019 the Octant contributors. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+//
+
+import { Component, Input } from '@angular/core';
+import { View } from 'src/app/modules/shared/models/content';
+import fixPassiveEvents from '../../fixPassiveEvents';
+
+@Component({
+  selector: 'app-content-switcher',
+  templateUrl: './content-switcher.component.html',
+  styleUrls: ['./content-switcher.component.scss'],
+})
+export class ContentSwitcherComponent {
+  @Input() view: View;
+  constructor() {
+    fixPassiveEvents();
+  }
+}
diff --git a/web/src/app/modules/shared/components/presentation/content-text-filter/content-text-filter.component.html b/web/src/app/modules/shared/components/presentation/content-text-filter/content-text-filter.component.html
index a54b8743..380618b5 100644
--- a/web/src/app/modules/shared/components/presentation/content-text-filter/content-text-filter.component.html
+++ b/web/src/app/modules/shared/components/presentation/content-text-filter/content-text-filter.component.html
@@ -4,10 +4,6 @@
   (ngModelChange)="onFilterChange($event)"
   [(ngModel)]="text"
 />
-<button
-  class="btn btn-sm btn-danger-outline reset-filter"
-  [disabled]="!isActive()"
-  (click)="reset()"
->
+<button  class="btn btn-sm btn-danger-outline reset-filter" [disabled]="!isActive()" (click)="reset()">
   Reset
-</button>
+</button>
\ No newline at end of file
diff --git a/web/src/app/modules/shared/components/presentation/content-text-filter/content-text-filter.component.scss b/web/src/app/modules/shared/components/presentation/content-text-filter/content-text-filter.component.scss
index 231cc390..9c6a8ffc 100644
--- a/web/src/app/modules/shared/components/presentation/content-text-filter/content-text-filter.component.scss
+++ b/web/src/app/modules/shared/components/presentation/content-text-filter/content-text-filter.component.scss
@@ -1,4 +1,4 @@
 .reset-filter {
-  display: block;
-  width: 100%;
-}
+    display: block;
+    width: 100%;
+}
\ No newline at end of file
diff --git a/web/src/app/modules/shared/components/presentation/cytoscape/cytoscape.component.ts b/web/src/app/modules/shared/components/presentation/cytoscape/cytoscape.component.ts
index 1a8e023d..3bb67cd0 100644
--- a/web/src/app/modules/shared/components/presentation/cytoscape/cytoscape.component.ts
+++ b/web/src/app/modules/shared/components/presentation/cytoscape/cytoscape.component.ts
@@ -46,7 +46,7 @@ export class CytoscapeComponent implements OnChanges, OnDestroy {
 
   private instance: cytoscape.Core;
 
-  constructor(private renderer: Renderer2) {
+  constructor(private renderer: Renderer2, private el: ElementRef) {
     this.layout = this.layout || {
       name: 'grid',
       directed: true,
@@ -63,7 +63,7 @@ export class CytoscapeComponent implements OnChanges, OnDestroy {
   }
 
   ngOnDestroy(): void {
-    if (this.instance && !this.instance.destroyed()) {
+    if (!this.instance.destroyed()) {
       this.instance.destroy();
     }
   }
diff --git a/web/src/app/modules/shared/components/presentation/cytoscape2/cytoscape2.component.ts b/web/src/app/modules/shared/components/presentation/cytoscape2/cytoscape2.component.ts
index 03a39b9f..2b2c60bc 100644
--- a/web/src/app/modules/shared/components/presentation/cytoscape2/cytoscape2.component.ts
+++ b/web/src/app/modules/shared/components/presentation/cytoscape2/cytoscape2.component.ts
@@ -50,7 +50,7 @@ export class Cytoscape2Component implements OnChanges {
   applied = false;
   moveStarted = false;
 
-  constructor(private renderer: Renderer2) {
+  constructor(private renderer: Renderer2, private el: ElementRef) {
     this.layout = this.layout || {
       name: 'grid',
       directed: true,
@@ -90,7 +90,7 @@ export class Cytoscape2Component implements OnChanges {
       localSelect.emit(node.data());
     });
 
-    this.cytoscape.on('layoutstop', _ => {
+    this.cytoscape.on('layoutstop', e => {
       if (!this.applied) {
         this.applied = true;
         this.cytoscape
diff --git a/web/src/app/modules/shared/components/presentation/cytoscape2/shape.ts b/web/src/app/modules/shared/components/presentation/cytoscape2/shape.ts
index d1e0c5e3..e1ddc150 100644
--- a/web/src/app/modules/shared/components/presentation/cytoscape2/shape.ts
+++ b/web/src/app/modules/shared/components/presentation/cytoscape2/shape.ts
@@ -1,16 +1,17 @@
+
 export abstract class BaseShape {
-  protected constructor(public id: string) {}
+  protected constructor(public id: string) {
+  }
 
   toNode(shapes: BaseShape[]) {}
 }
 
 export class Edge extends BaseShape {
-  constructor(
-    id: string,
-    public sourceId: string,
-    public targetId: string,
-    public classes?: string
-  ) {
+
+  constructor( id: string,
+                  public sourceId: string,
+                  public targetId: string,
+                  public classes?: string ) {
     super(id);
   }
 
@@ -28,9 +29,10 @@ export class Edge extends BaseShape {
       locked: false,
       grabbable: true,
       pannable: false,
-      classes: this.classes,
-    };
+      classes: this.classes
+    }
   }
+
 }
 
 export abstract class Shape extends BaseShape {
@@ -40,76 +42,68 @@ export abstract class Shape extends BaseShape {
 
   textMeasureCanvas: any;
 
-  constructor(
-    id: string,
-    public label: string,
-    public width: any,
-    public height: any,
-    public shape: string,
-    public hasChildren: boolean,
-    public parentId?: string
-  ) {
+  constructor(id: string,
+              public label: string,
+              public width: any,
+              public height: any,
+              public shape: string,
+              public hasChildren: boolean,
+              public parentId?: string) {
     super(id);
   }
 
-  abstract preferredPosition(): { x: number; y: number };
+  abstract preferredPosition(): {x: number, y: number};
 
   ports: Shape[] = [];
 
   nextPortPosition(shapes: BaseShape[], port: Port, prefered: number): number {
-    if (this.ports.length === 0) {
+    if(this.ports.length === 0) {
       this.ports.push(port);
       return prefered;
     }
 
-    if (this.ports.includes(port)) {
+    if(this.ports.includes(port)) {
       return port.y;
     }
 
-    const total = this.ports.filter(
-      (shape: Port) =>
-        shape.parentId === this.id && shape.location === port.location
-    ).length;
+    const total= this.ports.filter( (shape: Port) => shape.parentId === this.id && shape.location === port.location).length;
     this.ports.push(port);
     return total > 0 ? prefered + this.height / 3 : prefered;
   }
 
+
   preferredPortPosition(shapes: BaseShape[]): number {
-    return this.preferredPosition().y - this.height / 6;
+    return(this.preferredPosition().y - this.height / 6);
   }
 
-  getPosition(shapes: BaseShape[]): { x: number; y: number } {
+  getPosition(shapes: BaseShape[]): {x: number, y: number}{
     return this.preferredPosition();
   }
 
-  getPortPosition(shapes: Shape[], port: Port): { x: number; y: number } {
-    const textWidth = this.getTextWidth(port.label) + 10;
-    const { x, y } = this.getPosition(shapes);
-    const portY = this.nextPortPosition(
-      shapes,
-      port,
-      this.preferredPortPosition(shapes)
-    );
+  getPortPosition(shapes: Shape[], port: Port): {x: number, y: number}{
+    const textWidth= this.getTextWidth(port.label) + 10;
+    const {x, y} = this.getPosition(shapes);
+    const portY= this.nextPortPosition(shapes, port, this.preferredPortPosition(shapes));
 
     switch (port.location) {
       default:
       case 'left':
-        return { x: x - this.width / 2 + textWidth / 2, y: portY };
+        return {x: x - this.width / 2 + textWidth / 2, y: portY}
       case 'right':
-        return { x: x + this.width / 2 - textWidth / 2, y: portY };
+        return {x: x + this.width / 2 - textWidth / 2, y: portY}
     }
   }
 
-  isMovable(): boolean {
+  isMovable():boolean {
     return this.parentId == undefined;
   }
 
-  getTextWidth(txt) {
-    if (!this.textMeasureCanvas) {
-      this.textMeasureCanvas = document.createElement('canvas');
-    }
-    const context = this.textMeasureCanvas.getContext('2d');
-    context.font = '14px Metropolis';
+  getTextWidth(txt){
+    if(!this.textMeasureCanvas) {
+      this.textMeasureCanvas = document.createElement("canvas")
+    };
+    const context = this.textMeasureCanvas.getContext("2d");
+    context.font = "14px Metropolis";
     const metrics = context.measureText(txt);
     return metrics.width - 2;
   }
@@ -134,139 +128,102 @@ export abstract class Shape extends BaseShape {
       locked: false,
       grabbable: this.isMovable(),
       pannable: false,
-      classes: this.classes,
-    };
+      classes: this.classes
+    }
   }
 }
 
 export class Deployment extends Shape {
-  constructor(
-    id: string,
-    label: string,
-    hasChildren: boolean,
-    parentId?: string
-  ) {
+  constructor(id: string, label: string, hasChildren: boolean, parentId?: string) {
     super(id, label, 800, 600, 'rectangle', hasChildren, parentId);
-    this.classes = 'deployment';
+    this.classes= 'deployment';
   }
 
-  preferredPosition(): { x: number; y: number } {
-    return { x: 750, y: 450 };
-  }
+  preferredPosition(): {x: number, y: number} {
+    return{x: 750, y: 450}
+  };
 
   preferredPortPosition(shapes: BaseShape[]): number {
-    return this.preferredPosition().y / 2;
+    return(this.preferredPosition().y/2);
   }
+
 }
 
 export class Secret extends Shape {
-  constructor(
-    id: string,
-    label: string,
-    hasChildren: boolean,
-    parentId?: string
-  ) {
+  constructor(id: string, label: string, hasChildren: boolean, parentId?: string) {
     super(id, label, 350, 200, 'roundrectangle', hasChildren, parentId);
-    this.classes = 'secret';
+    this.classes= 'secret';
   }
 
-  preferredPosition(): { x: number; y: number } {
-    return { x: 100, y: 1000 };
-  }
+  preferredPosition(): {x: number, y: number} {
+    return{x: 100, y: 1000}
+  };
 }
 
 export class ServiceAccount extends Shape {
-  constructor(
-    id: string,
-    label: string,
-    hasChildren: boolean,
-    parentId?: string
-  ) {
+  constructor(id: string, label: string, hasChildren: boolean, parentId?: string) {
     super(id, label, 350, 200, 'roundrectangle', hasChildren, parentId);
-    this.classes = 'secret';
+    this.classes= 'secret';
   }
 
-  preferredPosition(): { x: number; y: number } {
-    return { x: 600, y: 1000 };
-  }
+  preferredPosition(): {x: number, y: number} {
+    return{x: 600, y: 1000}
+  };
 }
 
 export class Service extends Shape {
-  constructor(
-    id: string,
-    label: string,
-    hasChildren: boolean,
-    parentId?: string
-  ) {
+  constructor(id: string, label: string, hasChildren: boolean, parentId?: string) {
     super(id, label, 350, 200, 'roundrectangle', hasChildren, parentId);
-    this.classes = 'secret';
+    this.classes= 'secret';
   }
 
-  preferredPosition(): { x: number; y: number } {
-    return { x: 0, y: 400 };
-  }
+  preferredPosition(): {x: number, y: number} {
+    return{x: 0, y: 400}
+  };
 }
 
 export class ReplicaSet extends Shape {
-  constructor(
-    id: string,
-    label: string,
-    hasChildren: boolean,
-    parentId?: string
-  ) {
+  constructor(id: string, label: string, hasChildren: boolean, parentId?: string) {
     super(id, label, 600, 400, 'rectangle', hasChildren, parentId);
-    this.classes = 'replicaset';
+    this.classes= 'replicaset';
   }
 
-  preferredPosition(): { x: number; y: number } {
-    return { x: 800, y: 500 };
-  }
+  preferredPosition(): {x: number, y: number} {
+    return{x: 800, y: 500}
+  };
 }
 
 export class Pod extends Shape {
-  constructor(
-    id: string,
-    label: string,
-    hasChildren: boolean,
-    parentId?: string
-  ) {
+  constructor(id: string, label: string, hasChildren: boolean, parentId?: string) {
     super(id, label, 350, 200, 'roundrectangle', hasChildren, parentId);
-    this.classes = 'pod';
+    this.classes= 'pod';
   }
 
-  preferredPosition(): { x: number; y: number } {
-    return { x: 875, y: 525 };
-  }
+  preferredPosition(): {x: number, y: number} {
+    return{x: 875, y: 525}
+  };
 }
 
 export class Port extends Shape {
-  constructor(
-    id: string,
-    label: string,
-    public location: string,
-    className: string,
-    parentId?: string
-  ) {
+  constructor(id: string, label: string, public location: string, className: string, parentId?: string) {
     super(id, label, 'label', 'label', 'rectangle', false, parentId);
-    this.classes = className;
+    this.classes= className;
   }
 
-  isMovable(): boolean {
+  isMovable():boolean {
     return false;
   }
 
-  preferredPosition(): { x: number; y: number } {
-    return { x: 750, y: 450 };
-  }
+  preferredPosition(): {x: number, y: number} {
+    return{x: 750, y: 450}
+  };
 
-  getPosition(shapes: Shape[]): { x: number; y: number } {
-    const parentNode: Shape = shapes.find(
-      (shape: Shape) => shape.id === this.parentId
-    );
+  getPosition(shapes: Shape[]): {x: number, y: number} {
+    const parentNode: Shape= shapes.find( (shape: Shape) => shape.id===this.parentId);
     const portPosition = parentNode.getPortPosition(shapes, this);
-    this.x = portPosition.x;
-    this.y = portPosition.y;
+    this.x= portPosition.x;
+    this.y= portPosition.y;
 
     return portPosition;
-  }
+  };
 }
diff --git a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.html b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.html
index f9611c74..a415734d 100644
--- a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.html
+++ b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.html
@@ -1,27 +1,28 @@
 <h4 *ngIf="showTitle()">{{ title }}</h4>
-<clr-datagrid [clrDgLoading]="loading$ | async">
+<clr-datagrid>
   <clr-dg-placeholder>
     <ng-container *ngIf="placeholder?.length > 0; else emptyPlaceholder">
       {{ placeholder }}
     </ng-container>
     <ng-template #emptyPlaceholder>
-      <!--      All content has been filtered out.-->
+      All content has been filtered out.
     </ng-template>
   </clr-dg-placeholder>
-  <clr-dg-column
-    *ngFor="let columnName of columns; trackBy: identifyColumn"
-    [clrDgSortBy]="columnName === 'Age' ? timeStampComparator : null"
-    [(clrDgSortOrder)]="sortOrder"
+  <clr-dg-column *ngFor="let columnName of columns; trackBy: identifyColumn"
+                  [clrDgSortBy]="columnName === 'Age' ? timeStampComparator : null"
+                  [(clrDgSortOrder)]="sortOrder"
   >
     {{ columnName }}
     <clr-dg-filter *ngIf="filters[columnName]">
-      <app-content-filter
+      <app-content-filter 
         [column]="columnName"
         [filter]="filters[columnName]"
       ></app-content-filter>
     </clr-dg-filter>
     <clr-dg-filter *ngIf="!filters[columnName]">
-      <app-content-text-filter [column]="columnName"></app-content-text-filter>
+      <app-content-text-filter 
+        [column]="columnName"
+      ></app-content-text-filter>
     </clr-dg-filter>
   </clr-dg-column>
   <clr-dg-row
@@ -29,14 +30,14 @@
     [ngClass]="row | filterDeletedDatagridRow"
   >
     <clr-dg-action-overflow *ngIf="row.actions.length > 0 && !row.isDeleted">
-      <ng-container *ngFor="let action of row.actions; trackBy: identifyAction">
+      <ng-container *ngFor="let action of row.actions">
         <button class="action-item" (click)="runAction(action)">
           {{ action.name }}
         </button>
       </ng-container>
     </clr-dg-action-overflow>
     <clr-dg-cell *ngFor="let column of columns; trackBy: identifyColumn">
-      <app-view-container [view]="row.data[column]"></app-view-container>
+      <app-content-switcher [view]="row.data[column]"></app-content-switcher>
     </clr-dg-cell>
   </clr-dg-row>
   <clr-dg-footer>
@@ -49,6 +50,12 @@
         {{ pagination.totalItems }} items
       </ng-container>
     </clr-dg-pagination>
+
+    <ng-container *ngIf="loading">
+      <span class="spinner spinner-inline" style="margin-right: 10px;">
+        Loading...
+      </span>
+    </ng-container>
   </clr-dg-footer>
 </clr-datagrid>
 
diff --git a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
index 3729f037..f3b6890f 100644
--- a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
+++ b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
@@ -4,12 +4,13 @@
 
 .row-deleted {
   :host-context(body.dark) {
-    --barberpole-color: hsl(9, 95%, 12%);
+    --barberpole-color: hsl(9, 95%, 12%)
   }
 
   :host-context(body) {
-    --barberpole-color: hsl(9, 83%, 76%);
+    --barberpole-color: hsl(9, 83%, 76%)
   }
 
   background-color: var(--barberpole-color);
 }
+
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 318e058f..3a599a34 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
@@ -3,11 +3,7 @@
 //
 
 import { ClrDatagridSortOrder } from '@clr/angular';
-import {
-  ChangeDetectionStrategy,
-  ChangeDetectorRef,
-  Component,
-} from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import {
   Confirmation,
   GridAction,
@@ -16,26 +12,32 @@ import {
   TableRow,
   TableRowWithMetadata,
   TableView,
+  View,
 } from 'src/app/modules/shared/models/content';
 import trackByIndex from 'src/app/util/trackBy/trackByIndex';
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
 import { TimestampComparator } from '../../../../../util/timestamp-comparator';
 import { ViewService } from '../../../services/view/view.service';
 import { ActionService } from '../../../services/action/action.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
-import { BehaviorSubject, merge, Observable, timer } from 'rxjs';
-import { LoadingService } from '../../../services/loading/loading.service';
 
 @Component({
   selector: 'app-view-datagrid',
   templateUrl: './datagrid.component.html',
   styleUrls: ['./datagrid.component.scss'],
-  changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class DatagridComponent extends AbstractViewComponent<TableView> {
+export class DatagridComponent implements OnChanges {
+  private v: TableView;
   timeStampComparator = new TimestampComparator();
   sortOrder: ClrDatagridSortOrder = ClrDatagridSortOrder.UNSORTED;
 
+  @Input() set view(v: View) {
+    this.v = v as TableView;
+  }
+
+  get view() {
+    return this.v;
+  }
+
   columns: string[];
   rowsWithMetadata: TableRowWithMetadata[];
   title: string;
@@ -46,48 +48,43 @@ export class DatagridComponent extends AbstractViewComponent<TableView> {
 
   actionDialogOptions: ActionDialogOptions = undefined;
 
+  private previousView: SimpleChanges;
+
   identifyRow = trackByIndex;
   identifyColumn = trackByIdentity;
-  identifyAction = trackByIdentity;
-
   loading: boolean;
-  loading$: Observable<boolean>;
 
   constructor(
     private viewService: ViewService,
-    private actionService: ActionService,
-    private loadingService: LoadingService,
-    private cdr: ChangeDetectorRef
-  ) {
-    super();
-  }
-
-  update() {
-    this.title = this.viewService.viewTitleAsText(this.view);
-
-    this.loading = true;
-
-    const done = new BehaviorSubject(false);
-    this.loading$ = this.loadingService.withDelay(done, 250, 1000);
-
-    setTimeout(() => {
-      this.rowsWithMetadata = this.getRowsWithMetadata(this.v.config.rows);
-      this.placeholder = this.v.config.emptyContent;
-      this.lastUpdated = new Date();
-      this.loading = this.v.config.loading;
-      done.next(true);
-      done.complete();
-      this.cdr.markForCheck();
-    });
-    this.columns = this.v.config.columns.map(column => column.name);
-    this.filters = this.v.config.filters;
+    private actionService: ActionService
+  ) {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view) {
+      if (
+        JSON.stringify(this.previousView) !==
+        JSON.stringify(changes.view.currentValue)
+      ) {
+        this.title = this.viewService.viewTitleAsText(this.view);
+
+        const current = changes.view.currentValue as TableView;
+        this.columns = current.config.columns.map(column => column.name);
+
+        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;
+        this.filters = current.config.filters;
+
+        this.previousView = changes.view.currentValue;
+      }
+    }
   }
 
   private getRowsWithMetadata(rows: TableRow[]): TableRowWithMetadata[] {
-    if (!rows) {
-      return [];
-    }
-
     return rows.map(row => {
       let actions: GridAction[] = [];
 
diff --git a/web/src/app/modules/shared/components/presentation/donut-chart/donut-chart.component.html b/web/src/app/modules/shared/components/presentation/donut-chart/donut-chart.component.html
index 6d4d0006..67afd1b5 100644
--- a/web/src/app/modules/shared/components/presentation/donut-chart/donut-chart.component.html
+++ b/web/src/app/modules/shared/components/presentation/donut-chart/donut-chart.component.html
@@ -1,9 +1,4 @@
-<svg
-  [attr.viewBox]="viewBox()"
-  class="donut"
-  [attr.height]="scale"
-  [attr.width]="scale"
->
+<svg [attr.viewBox]="viewBox()" class="donut" [attr.height]="scale" [attr.width]="scale">
   <circle
     [attr.cx]="center()"
     [attr.cy]="center()"
diff --git a/web/src/app/modules/shared/components/presentation/donut-chart/donut-chart.component.ts b/web/src/app/modules/shared/components/presentation/donut-chart/donut-chart.component.ts
index d7d3d1a5..8278f065 100644
--- a/web/src/app/modules/shared/components/presentation/donut-chart/donut-chart.component.ts
+++ b/web/src/app/modules/shared/components/presentation/donut-chart/donut-chart.component.ts
@@ -3,9 +3,8 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import { Component, Input } from '@angular/core';
-import { DonutChartView, DonutSegment } from '../../../models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnInit } from '@angular/core';
+import { DonutChartView, DonutSegment, View } from '../../../models/content';
 
 export interface SegmentDescriptor {
   array: string;
@@ -18,16 +17,23 @@ export interface SegmentDescriptor {
   templateUrl: './donut-chart.component.html',
   styleUrls: ['./donut-chart.component.scss'],
 })
-export class DonutChartComponent extends AbstractViewComponent<DonutChartView> {
-  @Input() circumference = 100;
-  height = 42;
+export class DonutChartComponent implements OnInit {
+  v: DonutChartView;
   scale: string;
 
-  constructor() {
-    super();
+  @Input() set view(v: View) {
+    this.v = v as DonutChartView;
   }
+  get view() {
+    return this.v;
+  }
+
+  @Input() circumference = 100;
+  height = 42;
+
+  constructor() {}
 
-  update() {
+  ngOnInit() {
     if (this.v?.config?.size) {
       this.scale = String(this.v.config.size) + '%';
     }
diff --git a/web/src/app/modules/shared/components/presentation/error/error.component.ts b/web/src/app/modules/shared/components/presentation/error/error.component.ts
index 447c5943..6cd45536 100644
--- a/web/src/app/modules/shared/components/presentation/error/error.component.ts
+++ b/web/src/app/modules/shared/components/presentation/error/error.component.ts
@@ -2,23 +2,32 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
-import { ErrorView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { ErrorView, View } from 'src/app/modules/shared/models/content';
 
 @Component({
   selector: 'app-view-error',
   templateUrl: './error.component.html',
   styleUrls: ['./error.component.scss'],
 })
-export class ErrorComponent extends AbstractViewComponent<ErrorView> {
-  source: string;
+export class ErrorComponent implements OnChanges {
+  private v: ErrorView;
 
-  constructor() {
-    super();
+  @Input() set view(v: View) {
+    this.v = v as ErrorView;
+  }
+  get view() {
+    return this.v;
   }
 
-  update() {
-    this.source = this.v.config.data;
+  source: string;
+
+  constructor() {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as ErrorView;
+      this.source = view.config.data;
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/expression-selector/expression-selector.component.ts b/web/src/app/modules/shared/components/presentation/expression-selector/expression-selector.component.ts
index de2852dd..1c6a7203 100644
--- a/web/src/app/modules/shared/components/presentation/expression-selector/expression-selector.component.ts
+++ b/web/src/app/modules/shared/components/presentation/expression-selector/expression-selector.component.ts
@@ -2,30 +2,39 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
-import { ExpressionSelectorView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import {
+  ExpressionSelectorView,
+  View,
+} from 'src/app/modules/shared/models/content';
 
 @Component({
   selector: 'app-view-expression-selector',
   templateUrl: './expression-selector.component.html',
   styleUrls: ['./expression-selector.component.scss'],
 })
-export class ExpressionSelectorComponent extends AbstractViewComponent<
-  ExpressionSelectorView
-> {
+export class ExpressionSelectorComponent implements OnChanges {
+  private v: ExpressionSelectorView;
+
+  @Input() set view(v: View) {
+    this.v = v as ExpressionSelectorView;
+  }
+  get view() {
+    return this.v;
+  }
+
   key: string;
   operator: string;
   values: string;
 
-  constructor() {
-    super();
-  }
+  constructor() {}
 
-  update() {
-    const view = this.v;
-    this.key = view.config.key;
-    this.operator = view.config.operator;
-    this.values = view.config.values?.join(',');
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as ExpressionSelectorView;
+      this.key = view.config.key;
+      this.operator = view.config.operator;
+      this.values = view.config.values?.join(',');
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/flexlayout/flexlayout.component.html b/web/src/app/modules/shared/components/presentation/flexlayout/flexlayout.component.html
index e57adca8..dfc78251 100644
--- a/web/src/app/modules/shared/components/presentation/flexlayout/flexlayout.component.html
+++ b/web/src/app/modules/shared/components/presentation/flexlayout/flexlayout.component.html
@@ -16,7 +16,7 @@
       *ngFor="let item of section; trackBy: identifySection"
       class="clr-col-md-{{ item.width / 2 }} content-card-parent"
     >
-      <app-view-container [view]="item.view"></app-view-container>
+      <app-content-switcher [view]="item.view"></app-content-switcher>
     </div>
   </div>
 </ng-container>
diff --git a/web/src/app/modules/shared/components/presentation/flexlayout/flexlayout.component.ts b/web/src/app/modules/shared/components/presentation/flexlayout/flexlayout.component.ts
index 60a54941..bbf29767 100644
--- a/web/src/app/modules/shared/components/presentation/flexlayout/flexlayout.component.ts
+++ b/web/src/app/modules/shared/components/presentation/flexlayout/flexlayout.component.ts
@@ -2,42 +2,36 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
+import { Component, Input } from '@angular/core';
 import {
   ButtonGroupView,
   FlexLayoutItem,
+  FlexLayoutView,
+  View,
 } from 'src/app/modules/shared/models/content';
 import trackByIndex from 'src/app/util/trackBy/trackByIndex';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-flexlayout',
   templateUrl: './flexlayout.component.html',
   styleUrls: ['./flexlayout.component.scss'],
 })
-export class FlexlayoutComponent extends AbstractViewComponent<any> {
-  buttonGroup: ButtonGroupView;
-
-  sections: FlexLayoutItem[][];
-
-  identifySection = trackByIndex;
-
-  constructor() {
-    super();
-  }
+export class FlexlayoutComponent {
+  v: FlexLayoutView;
 
-  update() {
+  @Input() set view(v: View) {
+    this.v = v as FlexLayoutView;
     this.buttonGroup = this.v.config.buttonGroup;
     this.sections = this.v.config.sections;
   }
 
-  sectionStyle(item: FlexLayoutItem) {
-    return ['height', 'margin'].reduce((previousValue, currentValue) => {
-      if (!item[currentValue]) {
-        return previousValue;
-      }
-
-      return { ...previousValue, [currentValue]: item[currentValue] };
-    }, {});
+  get view() {
+    return this.v;
   }
+
+  buttonGroup: ButtonGroupView;
+
+  sections: FlexLayoutItem[][];
+
+  identifySection = trackByIndex;
 }
diff --git a/web/src/app/modules/shared/components/presentation/graphviz/graphviz.component.ts b/web/src/app/modules/shared/components/presentation/graphviz/graphviz.component.ts
index 798a8e9d..db720f9c 100644
--- a/web/src/app/modules/shared/components/presentation/graphviz/graphviz.component.ts
+++ b/web/src/app/modules/shared/components/presentation/graphviz/graphviz.component.ts
@@ -6,13 +6,15 @@ import {
   ChangeDetectionStrategy,
   Component,
   ElementRef,
+  Input,
+  OnChanges,
+  SimpleChanges,
   ViewChild,
   ViewEncapsulation,
 } from '@angular/core';
 
-import { GraphvizView } from 'src/app/modules/shared/models/content';
+import { GraphvizView, View } from 'src/app/modules/shared/models/content';
 import { D3GraphvizService } from '../../../services/d3/d3graphviz.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-graphviz',
@@ -21,17 +23,27 @@ import { AbstractViewComponent } from '../../abstract-view/abstract-view.compone
   encapsulation: ViewEncapsulation.None,
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class GraphvizComponent extends AbstractViewComponent<GraphvizView> {
+export class GraphvizComponent implements OnChanges {
   @ViewChild('viewer', { static: true }) private viewer: ElementRef;
 
-  constructor(private d3GraphvizService: D3GraphvizService) {
-    super();
+  private v: GraphvizView;
+
+  @Input() set view(v: View) {
+    this.v = v as GraphvizView;
+  }
+  get view() {
+    return this.v;
   }
 
-  update() {
-    const current = this.v.config.dot;
-    if (current) {
-      this.d3GraphvizService.render(this.viewer, current);
+  constructor(private d3GraphvizService: D3GraphvizService) {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as GraphvizView;
+      const current = view.config.dot;
+      if (current) {
+        this.d3GraphvizService.render(this.viewer, current);
+      }
     }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/iframe/iframe.component.ts b/web/src/app/modules/shared/components/presentation/iframe/iframe.component.ts
index 47e17758..c01d2ada 100644
--- a/web/src/app/modules/shared/components/presentation/iframe/iframe.component.ts
+++ b/web/src/app/modules/shared/components/presentation/iframe/iframe.component.ts
@@ -1,22 +1,31 @@
-import { Component } from '@angular/core';
-import { IFrameView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { IFrameView, View } from 'src/app/modules/shared/models/content';
 
 @Component({
   selector: 'app-iframe',
   templateUrl: './iframe.component.html',
   styleUrls: ['./iframe.component.scss'],
 })
-export class IFrameComponent extends AbstractViewComponent<IFrameView> {
+export class IFrameComponent implements OnChanges {
+  private v: IFrameView;
+
+  @Input() set view(v: View) {
+    this.v = v as IFrameView;
+  }
+  get view() {
+    return this.v;
+  }
+
   url: string;
   title: string;
 
-  constructor() {
-    super();
-  }
+  constructor() {}
 
-  update() {
-    this.url = this.v.config.url;
-    this.title = this.v.config.title;
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as IFrameView;
+      this.url = view.config.url;
+      this.title = view.config.title;
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
index 8db0d39a..ce4a7158 100644
--- a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
+++ b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
@@ -3,7 +3,7 @@
     <clr-signpost>
       <div [ngClass]="[currentStatus, 'indicator']" clrSignpostTrigger></div>
       <clr-signpost-content *clrIfOpen>
-        <app-view-container [view]="detail"></app-view-container>
+        <app-content-switcher [view]="detail"></app-content-switcher>
       </clr-signpost-content>
     </clr-signpost>
   </ng-container>
diff --git a/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.ts b/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.ts
index e3abff68..a3129d67 100644
--- a/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.ts
+++ b/web/src/app/modules/shared/components/presentation/label-selector/label-selector.component.ts
@@ -2,27 +2,41 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
-import { LabelSelectorView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { LabelSelectorView, View } from 'src/app/modules/shared/models/content';
 
 @Component({
   selector: 'app-view-label-selector',
   templateUrl: './label-selector.component.html',
   styleUrls: ['./label-selector.component.scss'],
 })
-export class LabelSelectorComponent extends AbstractViewComponent<
-  LabelSelectorView
-> {
+export class LabelSelectorComponent implements OnChanges {
+  private v: LabelSelectorView;
+  private previousView: SimpleChanges;
+
+  @Input() set view(v: View) {
+    this.v = v as LabelSelectorView;
+  }
+  get view() {
+    return this.v;
+  }
   key: string;
   value: string;
 
-  constructor() {
-    super();
-  }
+  constructor() {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      if (
+        JSON.stringify(this.previousView) !==
+        JSON.stringify(changes.view.currentValue)
+      ) {
+        const view = changes.view.currentValue as LabelSelectorView;
+        this.key = view.config.key;
+        this.value = view.config.value;
 
-  update() {
-    this.key = this.v.config.key;
-    this.value = this.v.config.value;
+        this.previousView = changes.view.currentValue;
+      }
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/labels/labels.component.ts b/web/src/app/modules/shared/components/presentation/labels/labels.component.ts
index 1a7c6c73..67253c05 100644
--- a/web/src/app/modules/shared/components/presentation/labels/labels.component.ts
+++ b/web/src/app/modules/shared/components/presentation/labels/labels.component.ts
@@ -2,30 +2,47 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component, Input } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import { LabelsView, View } from 'src/app/modules/shared/models/content';
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
 import { ViewService } from '../../../services/view/view.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-labels',
   templateUrl: './labels.component.html',
   styleUrls: ['./labels.component.scss'],
 })
-export class LabelsComponent extends AbstractViewComponent<LabelsView> {
+export class LabelsComponent implements OnChanges {
+  private v: LabelsView;
+  private previousView: SimpleChanges;
+
+  @Input() set view(v: View) {
+    this.v = v as LabelsView;
+  }
+  get view() {
+    return this.v;
+  }
+
   title: string;
   labelKeys: string[];
   labels: { [key: string]: string };
   trackByIdentity = trackByIdentity;
 
-  constructor(private viewService: ViewService) {
-    super();
-  }
+  constructor(private viewService: ViewService) {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      if (
+        JSON.stringify(this.previousView) !==
+        JSON.stringify(changes.view.currentValue)
+      ) {
+        const view = changes.view.currentValue as LabelsView;
+
+        this.title = this.viewService.viewTitleAsText(view);
+        this.labels = view.config.labels;
 
-  update() {
-    const view = this.v;
-    this.title = this.viewService.viewTitleAsText(view);
-    this.labels = view.config.labels;
+        this.previousView = changes.view.currentValue;
+      }
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/link/link.component.ts b/web/src/app/modules/shared/components/presentation/link/link.component.ts
index 91645894..bc64607f 100644
--- a/web/src/app/modules/shared/components/presentation/link/link.component.ts
+++ b/web/src/app/modules/shared/components/presentation/link/link.component.ts
@@ -2,9 +2,8 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
-import { LinkView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { LinkView, View } from 'src/app/modules/shared/models/content';
 
 const isUrlAbsolute = url =>
   url?.indexOf('://') > 0 || url?.indexOf('//') === 0;
@@ -14,24 +13,33 @@ const isUrlAbsolute = url =>
   templateUrl: './link.component.html',
   styleUrls: ['./link.component.scss'],
 })
-export class LinkComponent extends AbstractViewComponent<LinkView> {
+export class LinkComponent implements OnChanges {
+  private v: LinkView;
+
+  @Input() set view(v: View) {
+    this.v = v as LinkView;
+  }
+  get view() {
+    return this.v;
+  }
+
   ref: string;
   value: string;
   isAbsolute: boolean;
   hasStatus: boolean;
 
-  constructor() {
-    super();
-  }
+  constructor() {}
 
-  update() {
-    const view = this.v;
-    this.ref = view.config.ref;
-    this.value = view.config.value;
-    this.isAbsolute = isUrlAbsolute(this.ref);
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as LinkView;
+      this.ref = view.config.ref;
+      this.value = view.config.value;
+      this.isAbsolute = isUrlAbsolute(this.ref);
 
-    if (view.config.status) {
-      this.hasStatus = true;
+      if (view.config.status) {
+        this.hasStatus = true;
+      }
     }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/list/list.component.html b/web/src/app/modules/shared/components/presentation/list/list.component.html
index 2ccb31a0..ce8f7628 100644
--- a/web/src/app/modules/shared/components/presentation/list/list.component.html
+++ b/web/src/app/modules/shared/components/presentation/list/list.component.html
@@ -1,7 +1,5 @@
-<app-breadcrumb [path]="title"> </app-breadcrumb>
+<app-breadcrumb [path]="title">
+</app-breadcrumb>
 <ng-container *ngFor="let item of v?.config.items; trackBy: identifyItem">
-  <app-view-container
-    [view]="item"
-    (viewInit)="initDone()"
-  ></app-view-container>
+  <app-content-switcher [view]="item"></app-content-switcher>
 </ng-container>
diff --git a/web/src/app/modules/shared/components/presentation/list/list.component.ts b/web/src/app/modules/shared/components/presentation/list/list.component.ts
index 1b054aad..34fdb5ed 100644
--- a/web/src/app/modules/shared/components/presentation/list/list.component.ts
+++ b/web/src/app/modules/shared/components/presentation/list/list.component.ts
@@ -1,7 +1,7 @@
 // Copyright (c) 2019 the Octant contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 //
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import {
   LinkView,
   ListView,
@@ -10,44 +10,49 @@ import {
 
 import { IconService } from '../../../services/icon/icon.service';
 import { ViewService } from '../../../services/view/view.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-list',
   templateUrl: './list.component.html',
   styleUrls: ['./list.component.scss'],
-  changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ListComponent extends AbstractViewComponent<ListView> {
-  title: { title: string; url: string }[];
+export class ListComponent implements OnChanges {
+  v: ListView;
+
+  @Input() set view(v: View) {
+    this.v = v as ListView;
+  }
+  get view() {
+    return this.v;
+  }
+  title: string;
 
   iconName: string;
 
   constructor(
     private iconService: IconService,
     private viewService: ViewService
-  ) {
-    super();
-  }
+  ) {}
 
   identifyItem = (index: number, item: View): string => {
     return this.viewService.titleAsText(item.metadata.title);
   };
 
-  update() {
-    const current = this.v;
-    this.title = current.metadata.title
-      ? current.metadata.title.map((item: LinkView) => ({
-          title: item.config.value,
-          url: item.config.ref,
-        }))
-      : [];
-
-    if (this.v.config.items) {
-      this.initialChildCount = this.v.config.items.length;
-      this.v.config.items.forEach(item => {
-        item.totalItems = this.v.config.items.length;
-      });
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view) {
+      const current = changes.view.currentValue;
+      this.title = current.metadata.title
+        ? current.metadata.title.map((item: LinkView) => ({
+            title: item.config.value,
+            url: item.config.ref,
+          }))
+        : '';
+
+      if (this.v.config.items) {
+        this.v.config.items.forEach(item => {
+          item.totalItems = this.v.config.items.length;
+        });
+      }
     }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/loading/loading.component.ts b/web/src/app/modules/shared/components/presentation/loading/loading.component.ts
index 3e63af7d..ff4ea2b1 100644
--- a/web/src/app/modules/shared/components/presentation/loading/loading.component.ts
+++ b/web/src/app/modules/shared/components/presentation/loading/loading.component.ts
@@ -2,23 +2,32 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component, Input } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import { LoadingView, View } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-loading',
   templateUrl: './loading.component.html',
   styleUrls: ['./loading.component.scss'],
 })
-export class LoadingComponent extends AbstractViewComponent<LoadingView> {
-  value: string;
+export class LoadingComponent implements OnChanges {
+  private v: LoadingView;
 
-  constructor() {
-    super();
+  @Input() set view(v: View) {
+    this.v = v as LoadingView;
+  }
+  get view() {
+    return this.v;
   }
 
-  update() {
-    this.value = this.v.config.value;
+  value: string;
+
+  constructor() {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as LoadingView;
+      this.value = view.config.value;
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.html b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.html
index 74e51e65..952b1767 100644
--- a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.html
+++ b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.html
@@ -6,15 +6,15 @@
     <div class="card-block">
       <div class="card-title">
         <span *ngIf="node && !node.path">{{ node.name }}</span>
-        <app-view-container
+        <app-content-switcher
           *ngIf="node && node.path"
           [view]="node.path"
-        ></app-view-container>
+        ></app-content-switcher>
       </div>
       <div *ngIf="node" class="card-text">
         <ul class="node-status">
           <li *ngFor="let detail of node.details; trackBy: detailsTrackBy">
-            <app-view-container [view]="detail"></app-view-container>
+            <app-content-switcher [view]="detail"></app-content-switcher>
           </li>
         </ul>
       </div>
diff --git a/web/src/app/modules/shared/components/presentation/overflow-labels/overflow-labels.component.scss b/web/src/app/modules/shared/components/presentation/overflow-labels/overflow-labels.component.scss
index 73e3289d..50405f7e 100644
--- a/web/src/app/modules/shared/components/presentation/overflow-labels/overflow-labels.component.scss
+++ b/web/src/app/modules/shared/components/presentation/overflow-labels/overflow-labels.component.scss
@@ -13,7 +13,7 @@
     grid-auto-flow: column;
 
     .tooltip-wrapper {
-      margin: 0 0.3rem 0 0;
+      margin: 0 .3rem 0 0;
       overflow: hidden;
       text-overflow: ellipsis;
     }
diff --git a/web/src/app/modules/shared/components/presentation/overflow-selectors/overflow-selectors.component.scss b/web/src/app/modules/shared/components/presentation/overflow-selectors/overflow-selectors.component.scss
index 9888535b..5bbf9e5f 100644
--- a/web/src/app/modules/shared/components/presentation/overflow-selectors/overflow-selectors.component.scss
+++ b/web/src/app/modules/shared/components/presentation/overflow-selectors/overflow-selectors.component.scss
@@ -13,7 +13,7 @@
     grid-auto-flow: column;
 
     .tooltip-wrapper {
-      margin: 0 0.3rem 0 0;
+      margin: 0 .3rem 0 0;
       overflow: hidden;
       text-overflow: ellipsis;
     }
diff --git a/web/src/app/modules/shared/components/presentation/pod-status/pod-status.component.ts b/web/src/app/modules/shared/components/presentation/pod-status/pod-status.component.ts
index 84fd7fa7..a75a023f 100644
--- a/web/src/app/modules/shared/components/presentation/pod-status/pod-status.component.ts
+++ b/web/src/app/modules/shared/components/presentation/pod-status/pod-status.component.ts
@@ -5,12 +5,14 @@
 import {
   Component,
   ElementRef,
+  Input,
+  OnChanges,
+  SimpleChanges,
   ViewChild,
   ViewEncapsulation,
 } from '@angular/core';
-import { PodStatusView } from '../../../models/content';
+import { PodStatusView, View } from '../../../models/content';
 import { PodStatus } from '../../../models/pod-status';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-pod-status',
@@ -18,19 +20,26 @@ import { AbstractViewComponent } from '../../abstract-view/abstract-view.compone
   styleUrls: ['./pod-status.component.scss'],
   encapsulation: ViewEncapsulation.None,
 })
-export class PodStatusComponent extends AbstractViewComponent<PodStatusView> {
+export class PodStatusComponent implements OnChanges {
   @ViewChild('container') private container: ElementRef;
 
+  private v: PodStatusView;
+
+  @Input() set view(v: View) {
+    this.v = v as PodStatusView;
+  }
+  get view() {
+    return this.v;
+  }
+
   edgeLength = 7;
 
   podStatuses: PodStatus[] = [];
 
-  constructor() {
-    super();
-  }
+  constructor() {}
 
-  update() {
-    const pods = this.v.config.pods;
+  ngOnChanges(changes: SimpleChanges): void {
+    const pods = changes.view.currentValue.config.pods;
 
     if (pods) {
       this.podStatuses = Object.keys(pods)
diff --git a/web/src/app/modules/shared/components/presentation/port-forward/port-forward.component.ts b/web/src/app/modules/shared/components/presentation/port-forward/port-forward.component.ts
index 9d9bb97d..4c911309 100644
--- a/web/src/app/modules/shared/components/presentation/port-forward/port-forward.component.ts
+++ b/web/src/app/modules/shared/components/presentation/port-forward/port-forward.component.ts
@@ -2,21 +2,21 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
-import { PortForwardView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input } from '@angular/core';
+import { PortForwardView, View } from 'src/app/modules/shared/models/content';
 
 @Component({
   selector: 'app-view-port-forward',
   templateUrl: './port-forward.component.html',
   styleUrls: ['./port-forward.component.scss'],
 })
-export class PortForwardComponent extends AbstractViewComponent<
-  PortForwardView
-> {
-  constructor() {
-    super();
-  }
+export class PortForwardComponent {
+  private v: PortForwardView;
 
-  update() {}
+  @Input() set view(v: View) {
+    this.v = v as PortForwardView;
+  }
+  get view() {
+    return this.v;
+  }
 }
diff --git a/web/src/app/modules/shared/components/presentation/ports/ports.component.html b/web/src/app/modules/shared/components/presentation/ports/ports.component.html
index b3136137..0d98f1cc 100644
--- a/web/src/app/modules/shared/components/presentation/ports/ports.component.html
+++ b/web/src/app/modules/shared/components/presentation/ports/ports.component.html
@@ -2,12 +2,8 @@
   <div class="port" *ngFor="let port of v?.config.ports; trackBy: trackByFn">
     <div class="port-text">
       <span>{{ port.config.port }}/{{ port.config.protocol }}</span>
-      <span *ngIf="port.config.targetPort > 0 && !port.config.targetPortName">
-        ⟹ {{ port.config.targetPort }}</span
-      >
-      <span *ngIf="port.config.targetPort > 0 && port.config.targetPortName">
-        ⟹ {{ port.config.targetPortName }} ({{ port.config.targetPort }})</span
-      >
+      <span *ngIf="port.config.targetPort > 0 && !port.config.targetPortName"> ⟹ {{port.config.targetPort}}</span>
+      <span *ngIf="port.config.targetPort > 0 && port.config.targetPortName"> ⟹ {{port.config.targetPortName}} ({{port.config.targetPort}})</span>
     </div>
     <div class="port-actions">
       <ng-container *ngIf="port.config.state?.isForwarded">
diff --git a/web/src/app/modules/shared/components/presentation/ports/ports.component.ts b/web/src/app/modules/shared/components/presentation/ports/ports.component.ts
index 403f6389..8d9657d5 100644
--- a/web/src/app/modules/shared/components/presentation/ports/ports.component.ts
+++ b/web/src/app/modules/shared/components/presentation/ports/ports.component.ts
@@ -5,12 +5,14 @@
 import {
   Component,
   EventEmitter,
+  Input,
+  OnChanges,
   OnDestroy,
   Output,
+  SimpleChanges,
   ViewEncapsulation,
 } from '@angular/core';
-import { PortsView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { PortsView, View } from 'src/app/modules/shared/models/content';
 
 @Component({
   selector: 'app-view-ports',
@@ -18,29 +20,37 @@ import { AbstractViewComponent } from '../../abstract-view/abstract-view.compone
   styleUrls: ['./ports.component.scss'],
   encapsulation: ViewEncapsulation.None,
 })
-export class PortsComponent extends AbstractViewComponent<PortsView>
-  implements OnDestroy {
-  private previousView: PortsView;
+export class PortsComponent implements OnChanges, OnDestroy {
+  v: PortsView;
+
+  @Input() set view(v: View) {
+    this.v = v as PortsView;
+  }
+  get view() {
+    return this.v;
+  }
 
   @Output() portLoad: EventEmitter<boolean> = new EventEmitter(true);
 
-  constructor() {
-    super();
-  }
+  constructor() {}
 
-  update() {
-    if (JSON.stringify(this.v) !== JSON.stringify(this.previousView)) {
-      this.portLoad.emit(false);
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue && !changes.view.isFirstChange()) {
+      const currentView = changes.view.currentValue as PortsView;
+      const prevView = changes.view.previousValue as PortsView;
+      if (JSON.stringify(currentView) !== JSON.stringify(prevView)) {
+        this.portLoad.emit(false);
+      }
     }
   }
 
   ngOnDestroy() {}
 
-  load(_: Event) {
+  load(e: Event) {
     this.portLoad.emit(true);
   }
 
-  trackByFn(index, _) {
+  trackByFn(index, item) {
     return index;
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/quadrant/quadrant.component.ts b/web/src/app/modules/shared/components/presentation/quadrant/quadrant.component.ts
index 222a2435..aa03590b 100644
--- a/web/src/app/modules/shared/components/presentation/quadrant/quadrant.component.ts
+++ b/web/src/app/modules/shared/components/presentation/quadrant/quadrant.component.ts
@@ -2,13 +2,13 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import {
   QuadrantValue,
   QuadrantView,
+  View,
 } from 'src/app/modules/shared/models/content';
 import { ViewService } from '../../../services/view/view.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 const emptyQuadrantValue = { value: '', label: '' };
 
@@ -17,23 +17,32 @@ const emptyQuadrantValue = { value: '', label: '' };
   templateUrl: './quadrant.component.html',
   styleUrls: ['./quadrant.component.scss'],
 })
-export class QuadrantComponent extends AbstractViewComponent<QuadrantView> {
+export class QuadrantComponent implements OnChanges {
+  private v: QuadrantView;
+
+  @Input() set view(v: View) {
+    this.v = v as QuadrantView;
+  }
+  get view() {
+    return this.v;
+  }
+
   title: string;
   nw: QuadrantValue = emptyQuadrantValue;
   ne: QuadrantValue = emptyQuadrantValue;
   sw: QuadrantValue = emptyQuadrantValue;
   se: QuadrantValue = emptyQuadrantValue;
 
-  constructor(private viewService: ViewService) {
-    super();
-  }
+  constructor(private viewService: ViewService) {}
 
-  update() {
-    const view = this.v;
-    this.title = this.viewService.viewTitleAsText(view);
-    this.nw = view.config.nw;
-    this.ne = view.config.ne;
-    this.sw = view.config.sw;
-    this.se = view.config.se;
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as QuadrantView;
+      this.title = this.viewService.viewTitleAsText(view);
+      this.nw = view.config.nw;
+      this.ne = view.config.ne;
+      this.sw = view.config.sw;
+      this.se = view.config.se;
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/resource-viewer/resource-viewer.component.ts b/web/src/app/modules/shared/components/presentation/resource-viewer/resource-viewer.component.ts
index 17b8c8eb..be5be7c4 100644
--- a/web/src/app/modules/shared/components/presentation/resource-viewer/resource-viewer.component.ts
+++ b/web/src/app/modules/shared/components/presentation/resource-viewer/resource-viewer.component.ts
@@ -6,15 +6,17 @@ import {
   AfterViewInit,
   Component,
   Input,
+  OnChanges,
+  SimpleChanges,
   ViewEncapsulation,
 } from '@angular/core';
+import isEqual from 'lodash/isEqual';
 import {
   Node,
   ResourceViewerView,
   View,
 } from 'src/app/modules/shared/models/content';
 import { ElementsDefinition, Stylesheet } from 'cytoscape';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 const statusColorCodes = {
   ok: '#60b515',
@@ -30,9 +32,17 @@ const edgeColorCode = '#003d79';
   styleUrls: ['./resource-viewer.component.scss'],
   encapsulation: ViewEncapsulation.None,
 })
-export class ResourceViewerComponent
-  extends AbstractViewComponent<ResourceViewerView>
-  implements AfterViewInit {
+export class ResourceViewerComponent implements OnChanges, AfterViewInit {
+  private v: ResourceViewerView;
+
+  @Input() set view(v: View) {
+    this.v = v as ResourceViewerView;
+  }
+  get view() {
+    return this.v;
+  }
+
+  currentView: ResourceViewerView;
   selected: string;
   selectedNode: Node;
 
@@ -99,21 +109,26 @@ export class ResourceViewerComponent
 
   private afterFirstChange: boolean;
 
-  constructor() {
-    super();
-  }
+  constructor() {}
 
   ngAfterViewInit(): void {
     if (this.afterFirstChange) {
       this.graphData = this.generateGraphData();
     }
-    super.ngAfterViewInit();
   }
 
-  update() {
-    this.select(this.v.config.selected);
-    this.graphData = this.generateGraphData();
-    this.afterFirstChange = true;
+  ngOnChanges(changes: SimpleChanges): void {
+    const isEquals = isEqual(
+      changes.view.currentValue,
+      changes.view.previousValue
+    );
+
+    if (changes.view.isFirstChange() || !isEquals) {
+      this.currentView = changes.view.currentValue as ResourceViewerView;
+      this.select(this.currentView.config.selected);
+      this.graphData = this.generateGraphData();
+      this.afterFirstChange = true;
+    }
   }
 
   nodeChange(event) {
@@ -128,44 +143,48 @@ export class ResourceViewerComponent
   }
 
   private nodes() {
-    if (!this.v.config.nodes) {
+    if (!this.currentView || !this.currentView.config.nodes) {
       return [];
     }
 
-    const nodes = Object.entries(this.v.config.nodes).map(([name, details]) => {
-      const colorCode =
-        statusColorCodes[details.status] || statusColorCodes.error;
+    const nodes = Object.entries(this.currentView.config.nodes).map(
+      ([name, details]) => {
+        const colorCode =
+          statusColorCodes[details.status] || statusColorCodes.error;
 
-      return {
-        data: {
-          id: name,
-          name: `${details.name}\n${details.apiVersion} ${details.kind}`,
-          weight: 100,
-          colorCode,
-        },
-      };
-    });
+        return {
+          data: {
+            id: name,
+            name: `${details.name}\n${details.apiVersion} ${details.kind}`,
+            weight: 100,
+            colorCode,
+          },
+        };
+      }
+    );
 
     return Array.prototype.concat(...nodes);
   }
 
   private edges() {
-    if (!this.v.config.edges) {
+    if (!this.currentView || !this.currentView.config.edges) {
       return [];
     }
 
-    const edges = Object.entries(this.v.config.edges).map(([parent, maps]) => {
-      return maps.map(edge => {
-        return {
-          data: {
-            source: parent,
-            target: edge.node,
-            colorCode: edgeColorCode,
-            strength: 10,
-          },
-        };
-      });
-    });
+    const edges = Object.entries(this.currentView.config.edges).map(
+      ([parent, maps]) => {
+        return maps.map(edge => {
+          return {
+            data: {
+              source: parent,
+              target: edge.node,
+              colorCode: edgeColorCode,
+              strength: 10,
+            },
+          };
+        });
+      }
+    );
 
     return Array.prototype.concat(...edges);
   }
@@ -173,7 +192,7 @@ export class ResourceViewerComponent
   private select(id: string) {
     this.selected = id;
 
-    const nodes = this.v.config.nodes;
+    const nodes = this.currentView.config.nodes;
 
     if (nodes && nodes[id]) {
       this.selectedNode = nodes[id];
diff --git a/web/src/app/modules/shared/components/presentation/selectors/selectors.component.html b/web/src/app/modules/shared/components/presentation/selectors/selectors.component.html
index e1c1e57e..11c69879 100644
--- a/web/src/app/modules/shared/components/presentation/selectors/selectors.component.html
+++ b/web/src/app/modules/shared/components/presentation/selectors/selectors.component.html
@@ -1,6 +1,3 @@
 <div class="view-selectors">
-  <app-overflow-selectors
-    *ngIf="v?.config.selectors"
-    [selectors]="v?.config.selectors"
-  ></app-overflow-selectors>
+  <app-overflow-selectors *ngIf="v?.config.selectors" [selectors]="v?.config.selectors"></app-overflow-selectors>
 </div>
diff --git a/web/src/app/modules/shared/components/presentation/selectors/selectors.component.ts b/web/src/app/modules/shared/components/presentation/selectors/selectors.component.ts
index 0f4e318f..7a41bdea 100644
--- a/web/src/app/modules/shared/components/presentation/selectors/selectors.component.ts
+++ b/web/src/app/modules/shared/components/presentation/selectors/selectors.component.ts
@@ -2,25 +2,28 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
+import { Component, Input } from '@angular/core';
 import {
   ExpressionSelectorView,
   LabelSelectorView,
   SelectorsView,
+  View,
 } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-selectors',
   templateUrl: './selectors.component.html',
   styleUrls: ['./selectors.component.scss'],
 })
-export class SelectorsComponent extends AbstractViewComponent<SelectorsView> {
-  constructor() {
-    super();
-  }
+export class SelectorsComponent {
+  v: SelectorsView;
 
-  update() {}
+  @Input() set view(v: View) {
+    this.v = v as SelectorsView;
+  }
+  get view() {
+    return this.v;
+  }
 
   identifyItem(
     index: number,
diff --git a/web/src/app/modules/shared/components/presentation/single-stat/single-stat.component.ts b/web/src/app/modules/shared/components/presentation/single-stat/single-stat.component.ts
index acbe1b9d..60c53129 100644
--- a/web/src/app/modules/shared/components/presentation/single-stat/single-stat.component.ts
+++ b/web/src/app/modules/shared/components/presentation/single-stat/single-stat.component.ts
@@ -1,16 +1,22 @@
-import { Component } from '@angular/core';
-import { SingleStatView } from '../../../models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnInit } from '@angular/core';
+import { SingleStatView, View } from '../../../models/content';
 
 @Component({
   selector: 'app-single-stat',
   templateUrl: './single-stat.component.html',
   styleUrls: ['./single-stat.component.scss'],
 })
-export class SingleStatComponent extends AbstractViewComponent<SingleStatView> {
-  constructor() {
-    super();
+export class SingleStatComponent implements OnInit {
+  v: SingleStatView;
+
+  @Input() set view(v: View) {
+    this.v = v as SingleStatView;
+  }
+  get view() {
+    return this.v;
   }
 
-  update() {}
+  constructor() {}
+
+  ngOnInit() {}
 }
diff --git a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.html b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.html
index 04149ab5..a2eabff0 100644
--- a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.html
+++ b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.html
@@ -1,4 +1,8 @@
-<form clrStepper (ngSubmit)="onFormSubmit()" [formGroup]="formGroup">
+<form
+  clrStepper
+  (ngSubmit)="onFormSubmit()"
+  [formGroup]="formGroup"
+>
   <div
     class="step-item"
     *ngFor="let step of steps; index as i; trackBy: trackByFn"
@@ -8,103 +12,93 @@
       <clr-step-description>{{ step.description }}</clr-step-description>
       <clr-step-content>
         <div *ngFor="let field of step.form?.fields; trackBy: trackByFn">
-          <ng-container [ngSwitch]="field.type">
-            <ng-container *ngSwitchCase="'checkbox'">
-              <clr-checkbox-container>
-                <label [for]="field.name">{{ field.label }}</label>
-                <clr-checkbox-wrapper
-                  *ngFor="let opt of fieldChoices(field); trackBy: trackByFn"
-                >
-                  <input
-                    type="checkbox"
-                    clrCheckbox
-                    [formControlName]="field.name"
-                    [value]="opt.value"
-                    [checked]="opt.checked"
-                  />
-                  <label>{{ opt.label }}</label>
-                </clr-checkbox-wrapper>
-                <clr-control-error>{{ field.error }}</clr-control-error>
-              </clr-checkbox-container>
-            </ng-container>
-            <ng-container *ngSwitchCase="'radio'">
-              <clr-radio-container>
-                <label [for]="field.name">{{ field.label }}</label>
-                <clr-radio-wrapper
-                  *ngFor="let opt of fieldChoices(field); trackBy: trackByFn"
-                >
-                  <input
-                    type="radio"
-                    clrRadio
-                    [formControlName]="field.name"
-                    [value]="opt.value"
-                  />
-                  <label>{{ opt.label }}</label>
-                </clr-radio-wrapper>
-                <clr-control-error>{{ field.error }}</clr-control-error>
-              </clr-radio-container>
-            </ng-container>
-            <ng-container *ngSwitchCase="'text'">
-              <clr-input-container>
-                <label [for]="field.name">{{ field.label }}</label>
-                <input
-                  clrInput
-                  type="text"
-                  [name]="field.name"
-                  [formControlName]="field.name"
-                  [placeholder]="field.placeholder ? field.placeholder : ''"
-                />
-                <clr-control-error>{{ field.error }}</clr-control-error>
-              </clr-input-container>
-            </ng-container>
-            <ng-container *ngSwitchCase="'number'">
-              <clr-input-container>
-                <label [for]="field.name">{{ field.label }}</label>
-                <input clrInput type="number" [formControlName]="field.name" />
-                <clr-control-error>{{ field.error }}</clr-control-error>
-              </clr-input-container>
-            </ng-container>
-            <ng-container *ngSwitchCase="'password'">
-              <clr-input-container>
-                <label [for]="field.name">{{ field.label }}</label>
-                <input
-                  clrInput
-                  type="password"
-                  [formControlName]="field.name"
-                />
-                <clr-control-error>{{ field.error }}</clr-control-error>
-              </clr-input-container>
-            </ng-container>
-            <ng-container *ngSwitchCase="'select'">
-              <clr-select-container>
-                <label [for]="field.name">{{ field.label }}</label>
-                <select
-                  clrSelect
-                  [formControlName]="field.name"
-                  [multiple]="field.configuration.multiple"
-                >
-                  <option
-                    *ngFor="let opt of fieldChoices(field); trackBy: trackByFn"
-                    [value]="opt.value"
-                  >
-                    {{ opt.label }}
-                  </option>
-                </select>
-                <clr-control-error>{{ field.error }}</clr-control-error>
-              </clr-select-container>
-            </ng-container>
-            <ng-container *ngSwitchCase="'textarea'">
-              <clr-textarea-container>
-                <label [for]="field.name">{{ field.label }}</label>
-                <textarea clrTextarea [formControlName]="field.name"></textarea>
-                <clr-control-error>{{ field.error }}</clr-control-error>
-              </clr-textarea-container>
-            </ng-container>
-            <ng-container *ngSwitchCase="'hidden'"> </ng-container>
-            <ng-container *ngSwitchDefault>
-              Unable to display form field type {{ field.type }}
-            </ng-container>
-          </ng-container>
+            <ng-container [ngSwitch]="field.type">
+                <ng-container *ngSwitchCase="'checkbox'">
+                  <clr-checkbox-container>
+                    <label [for]="field.name">{{ field.label }}</label>
+                    <clr-checkbox-wrapper
+                      *ngFor="let opt of fieldChoices(field); trackBy: trackByFn"
+                    >
+                      <input
+                        type="checkbox"
+                        clrCheckbox
+                        [formControlName]="field.name"
+                        [value]="opt.value"
+                        [checked]="opt.checked"
+                      />
+                      <label>{{ opt.label }}</label>
+                    </clr-checkbox-wrapper>
+                    <clr-control-error>{{ field.error }}</clr-control-error>
+                  </clr-checkbox-container>
+                </ng-container>
+                <ng-container *ngSwitchCase="'radio'">
+                  <clr-radio-container>
+                    <label [for]="field.name">{{ field.label }}</label>
+                    <clr-radio-wrapper
+                      *ngFor="let opt of fieldChoices(field); trackBy: trackByFn"
+                    >
+                      <input
+                        type="radio"
+                        clrRadio
+                        [formControlName]="field.name"
+                        [value]="opt.value"
+                      />
+                      <label>{{ opt.label }}</label>
+                    </clr-radio-wrapper>
+                    <clr-control-error>{{ field.error }}</clr-control-error>
+                  </clr-radio-container>
+                </ng-container>
+                <ng-container *ngSwitchCase="'text'">
+                  <clr-input-container>
+                    <label [for]="field.name">{{ field.label }}</label>
+                    <input clrInput type="text" [name]="field.name" [formControlName]="field.name" [placeholder]="field.placeholder ? field.placeholder : ''" />
+                    <clr-control-error>{{ field.error }}</clr-control-error>
+                  </clr-input-container>
+                </ng-container>
+                <ng-container *ngSwitchCase="'number'">
+                  <clr-input-container>
+                    <label [for]="field.name">{{ field.label }}</label>
+                    <input clrInput type="number" [formControlName]="field.name" />
+                    <clr-control-error>{{ field.error }}</clr-control-error>
+                  </clr-input-container>
+                </ng-container>
+                <ng-container *ngSwitchCase="'password'">
+                  <clr-input-container>
+                    <label [for]="field.name">{{ field.label }}</label>
+                    <input clrInput type="password" [formControlName]="field.name" />
+                    <clr-control-error>{{ field.error }}</clr-control-error>
+                  </clr-input-container>
+                </ng-container>
+                <ng-container *ngSwitchCase="'select'">
+                  <clr-select-container>
+                    <label [for]="field.name">{{ field.label }}</label>
+                    <select
+                      clrSelect
+                      [formControlName]="field.name"
+                      [multiple]="field.configuration.multiple"
+                    >
+                      <option
+                        *ngFor="let opt of fieldChoices(field); trackBy: trackByFn"
+                        [value]="opt.value"
+                      >
+                        {{ opt.label }}
+                      </option>
+                    </select>
+                    <clr-control-error>{{ field.error }}</clr-control-error>
+                  </clr-select-container>
+                </ng-container>
+                <ng-container *ngSwitchCase="'textarea'">
+                  <clr-textarea-container>
+                    <label [for]="field.name">{{ field.label }}</label>
+                    <textarea clrTextarea [formControlName]="field.name"></textarea>
+                    <clr-control-error>{{ field.error }}</clr-control-error>
+                  </clr-textarea-container>
+                </ng-container>
+                <ng-container *ngSwitchCase="'hidden'"> </ng-container>
+                <ng-container *ngSwitchDefault>
+                  Unable to display form field type {{ field.type }}
+                </ng-container>
+              </ng-container>
         </div>
         <button
           class="submit"
diff --git a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts
index b6fa83ee..5aab2d8c 100644
--- a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts
+++ b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.ts
@@ -1,13 +1,27 @@
-import { Component, EventEmitter, isDevMode, Output } from '@angular/core';
-import { ActionField, StepItem, StepperView } from '../../../models/content';
 import {
-  FormBuilder,
+  Component,
+  OnInit,
+  Input,
+  SimpleChanges,
+  EventEmitter,
+  Output,
+  OnChanges,
+  OnDestroy,
+  isDevMode,
+} from '@angular/core';
+import {
+  StepperView,
+  View,
+  ActionField,
+  StepItem,
+} from '../../../models/content';
+import {
   FormGroup,
-  ValidatorFn,
+  FormBuilder,
   Validators,
+  ValidatorFn,
 } from '@angular/forms';
 import { WebsocketService } from '../../../services/websocket/websocket.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 interface Choice {
   label: string;
@@ -20,7 +34,17 @@ interface Choice {
   templateUrl: './stepper.component.html',
   styleUrls: ['./stepper.component.sass'],
 })
-export class StepperComponent extends AbstractViewComponent<StepperView> {
+export class StepperComponent implements OnInit {
+  private v: StepperView;
+
+  @Input() set view(v: View) {
+    this.v = v as StepperView;
+  }
+
+  get view() {
+    return this.v;
+  }
+
   @Output()
   submit: EventEmitter<FormGroup> = new EventEmitter(true);
 
@@ -29,21 +53,23 @@ export class StepperComponent extends AbstractViewComponent<StepperView> {
 
   formGroup: FormGroup;
   action: string;
-  steps: StepItem[] = [];
+  steps: StepItem[];
 
   constructor(
     private formBuilder: FormBuilder,
     private websocketService: WebsocketService
   ) {
-    super();
+    this.steps = [] as StepItem[];
   }
 
-  update() {
-    this.action = this.v.config.action;
-    this.steps = this.v.config.steps;
+  ngOnInit() {
+    if (this.v) {
+      this.action = this.v.config.action;
+      this.steps = this.v.config.steps;
 
-    const stepGroups = this.createStepGroups(this.steps);
-    this.formGroup = this.formBuilder.group(stepGroups);
+      const stepGroups = this.createStepGroups(this.steps);
+      this.formGroup = this.formBuilder.group(stepGroups);
+    }
   }
 
   createStepGroups(steps: StepItem[]): { [name: string]: any } {
@@ -84,7 +110,7 @@ export class StepperComponent extends AbstractViewComponent<StepperView> {
     this.cancel.emit(true);
   }
 
-  trackByFn(index, _) {
+  trackByFn(index, item) {
     return index;
   }
 
diff --git a/web/src/app/modules/shared/components/presentation/summary/summary.component.html b/web/src/app/modules/shared/components/presentation/summary/summary.component.html
index a468add0..c077c4b1 100644
--- a/web/src/app/modules/shared/components/presentation/summary/summary.component.html
+++ b/web/src/app/modules/shared/components/presentation/summary/summary.component.html
@@ -25,8 +25,59 @@
         <tbody>
           <tr *ngFor="let item of v?.config.sections; trackBy: identifyItem">
             <td class="left">{{ item.header }}</td>
-            <td class="left">
-              <app-view-container [view]="item.content"></app-view-container>
+            <td class="left" [ngSwitch]="item?.content?.metadata?.type">
+              <ng-container *ngSwitchCase="'annotations'">
+                <app-view-annotations
+                  [view]="item.content"
+                ></app-view-annotations>
+              </ng-container>
+              <ng-container *ngSwitchCase="'labels'">
+                <app-view-labels [view]="item.content"></app-view-labels>
+              </ng-container>
+              <ng-container *ngSwitchCase="'link'">
+                <app-view-link [view]="item.content"></app-view-link>
+              </ng-container>
+              <ng-container *ngSwitchCase="'selectors'">
+                <app-view-selectors [view]="item.content"></app-view-selectors>
+              </ng-container>
+              <ng-container *ngSwitchCase="'text'">
+                <app-view-text [view]="item.content"></app-view-text>
+              </ng-container>
+              <ng-container *ngSwitchCase="'timestamp'">
+                <app-view-timestamp [view]="item.content"></app-view-timestamp>
+              </ng-container>
+              <ng-container *ngSwitchCase="'table'">
+                <app-view-table [view]="item.content"></app-view-table>
+              </ng-container>
+              <ng-container *ngSwitchCase="'ports'">
+                <app-view-ports
+                  [view]="item.content"
+                  (portLoad)="onPortLoad($event)"
+                ></app-view-ports>
+              </ng-container>
+              <ng-container *ngSwitchCase="'list'">
+                <app-view-list [view]="item.content"></app-view-list>
+              </ng-container>
+              <ng-container *ngSwitchCase="'loading'">
+                <app-view-loading [view]="item.content"></app-view-loading>
+              </ng-container>
+              <ng-container *ngSwitchCase="'error'">
+                <app-view-error [view]="item.content"></app-view-error>
+              </ng-container>
+              <ng-container *ngSwitchCase="'graphviz'">
+                <app-view-graphviz [view]="item.content"></app-view-graphviz>
+              </ng-container>
+              <div class="alert alert-danger static" *ngSwitchDefault>
+                <span class="alert-icon-wrapper">
+                  <clr-icon
+                    class="alert-icon"
+                    shape="exclamation-circle"
+                  ></clr-icon>
+                </span>
+                <span class="alert-text">
+                  {{ item?.content?.metadata?.type }} not implemented
+                </span>
+              </div>
             </td>
           </tr>
         </tbody>
@@ -34,7 +85,7 @@
     </div>
     <div class="card-footer" *ngIf="shouldShowFooter()">
       <ng-container
-        *ngFor="let action of v.config.actions; trackBy: identifyItem"
+        *ngFor="let action of v?.config.actions; trackBy: identifyItem"
       >
         <button class="btn btn-sm btn-link" (click)="setAction(action)">
           {{ action.name }}
diff --git a/web/src/app/modules/shared/components/presentation/summary/summary.component.ts b/web/src/app/modules/shared/components/presentation/summary/summary.component.ts
index 7900bc58..bf41c198 100644
--- a/web/src/app/modules/shared/components/presentation/summary/summary.component.ts
+++ b/web/src/app/modules/shared/components/presentation/summary/summary.component.ts
@@ -2,23 +2,32 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import {
   Action,
   SummaryItem,
   SummaryView,
+  View,
 } from 'src/app/modules/shared/models/content';
 import { FormGroup } from '@angular/forms';
 import { ActionService } from '../../../services/action/action.service';
 import { ViewService } from '../../../services/view/view.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-summary',
   templateUrl: './summary.component.html',
   styleUrls: ['./summary.component.scss'],
 })
-export class SummaryComponent extends AbstractViewComponent<SummaryView> {
+export class SummaryComponent implements OnChanges {
+  private v: SummaryView;
+
+  @Input() set view(v: View) {
+    this.v = v as SummaryView;
+  }
+  get view() {
+    return this.v;
+  }
+
   title: string;
   isLoading = false;
 
@@ -27,13 +36,13 @@ export class SummaryComponent extends AbstractViewComponent<SummaryView> {
   constructor(
     private actionService: ActionService,
     private viewService: ViewService
-  ) {
-    super();
-  }
+  ) {}
 
-  update() {
-    const view = this.v;
-    this.title = this.viewService.viewTitleAsText(view);
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view: SummaryView = changes.view.currentValue;
+      this.title = this.viewService.viewTitleAsText(view);
+    }
   }
 
   identifyItem(index: number, item: SummaryItem): string {
diff --git a/web/src/app/modules/shared/components/presentation/table/table.component.html b/web/src/app/modules/shared/components/presentation/table/table.component.html
index 93177cef..6a843ed9 100644
--- a/web/src/app/modules/shared/components/presentation/table/table.component.html
+++ b/web/src/app/modules/shared/components/presentation/table/table.component.html
@@ -9,7 +9,7 @@
   <tbody>
     <tr *ngFor="let row of rows; trackBy: trackByIndex">
       <td class="left" *ngFor="let column of columns; trackBy: trackByIdentity">
-        <app-view-container [view]="row[column]"></app-view-container>
+        <app-content-switcher [view]="row[column]"></app-content-switcher>
       </td>
     </tr>
   </tbody>
diff --git a/web/src/app/modules/shared/components/presentation/table/table.component.ts b/web/src/app/modules/shared/components/presentation/table/table.component.ts
index bdce221f..07bfa122 100644
--- a/web/src/app/modules/shared/components/presentation/table/table.component.ts
+++ b/web/src/app/modules/shared/components/presentation/table/table.component.ts
@@ -2,19 +2,19 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import { TableRow, TableView } from 'src/app/modules/shared/models/content';
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
 import trackByIndex from 'src/app/util/trackBy/trackByIndex';
 import { ViewService } from '../../../services/view/view.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-table',
   templateUrl: './table.component.html',
   styleUrls: ['./table.component.scss'],
 })
-export class TableComponent extends AbstractViewComponent<TableView> {
+export class TableComponent implements OnChanges {
+  @Input() view: TableView;
   columns: string[];
   rows: TableRow[];
   title: string;
@@ -22,15 +22,15 @@ export class TableComponent extends AbstractViewComponent<TableView> {
   trackByIdentity = trackByIdentity;
   trackByIndex = trackByIndex;
 
-  constructor(private viewService: ViewService) {
-    super();
-  }
+  constructor(private viewService: ViewService) {}
 
-  update() {
-    const current = this.v;
-    this.title = this.viewService.viewTitleAsText(current);
-    this.columns = current.config.columns.map(column => column.name);
-    this.rows = current.config.rows;
-    this.placeholder = current.config.emptyContent;
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view) {
+      const current = changes.view.currentValue;
+      this.title = this.viewService.viewTitleAsText(current);
+      this.columns = current.config.columns.map(column => column.name);
+      this.rows = current.config.rows;
+      this.placeholder = current.config.emptyContent;
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/tabs/tabs.component.html b/web/src/app/modules/shared/components/presentation/tabs/tabs.component.html
index d77b092e..2598fa50 100644
--- a/web/src/app/modules/shared/components/presentation/tabs/tabs.component.html
+++ b/web/src/app/modules/shared/components/presentation/tabs/tabs.component.html
@@ -1,9 +1,10 @@
 <div class="clr-row clr-align-items-center clr-justify-content-between">
   <div class="clr-col-10">
-    <app-breadcrumb [path]="title"> </app-breadcrumb>
+    <app-breadcrumb [path]="title">
+    </app-breadcrumb>
   </div>
   <div class="clr-col-2">
-    <div *ngIf="buttonGroup" class="action-buttons">
+    <div class="action-buttons">
       <app-button-group [view]="buttonGroup"></app-button-group>
     </div>
   </div>
@@ -22,7 +23,7 @@
     </div>
     <ng-template [clrIfActive]="activeTab === tab.accessor">
       <clr-tab-content>
-        <app-view-container [view]="tab.view"></app-view-container>
+        <app-content-switcher [view]="tab.view"></app-content-switcher>
       </clr-tab-content>
     </ng-template>
   </clr-tab>
diff --git a/web/src/app/modules/shared/components/presentation/tabs/tabs.component.ts b/web/src/app/modules/shared/components/presentation/tabs/tabs.component.ts
index 4dd17ae4..52c92dd5 100644
--- a/web/src/app/modules/shared/components/presentation/tabs/tabs.component.ts
+++ b/web/src/app/modules/shared/components/presentation/tabs/tabs.component.ts
@@ -10,9 +10,9 @@ import {
 } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import {
+  View,
   ButtonGroupView,
   PathItem,
-  View,
 } from 'src/app/modules/shared/models/content';
 import { SliderService } from 'src/app/modules/shared/slider/slider.service';
 import { ViewService } from '../../../services/view/view.service';
diff --git a/web/src/app/modules/shared/components/presentation/text/text.component.spec.ts b/web/src/app/modules/shared/components/presentation/text/text.component.spec.ts
index 831d386a..1e8ac341 100644
--- a/web/src/app/modules/shared/components/presentation/text/text.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/text/text.component.spec.ts
@@ -7,7 +7,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 import { TextView } from '../../../models/content';
 import { TextComponent } from './text.component';
 import { Status } from '../indicator/indicator.component';
-import { ClarityModule } from '@clr/angular';
 
 @Component({
   template: '<app-view-text [view]="view"></app-view-text>',
@@ -24,7 +23,6 @@ describe('TextComponent', () => {
     beforeEach(async(() => {
       TestBed.configureTestingModule({
         declarations: [TestWrapperComponent, TextComponent],
-        imports: [ClarityModule],
       }).compileComponents();
     }));
 
diff --git a/web/src/app/modules/shared/components/presentation/text/text.component.ts b/web/src/app/modules/shared/components/presentation/text/text.component.ts
index ca21ec60..4e5aaf19 100644
--- a/web/src/app/modules/shared/components/presentation/text/text.component.ts
+++ b/web/src/app/modules/shared/components/presentation/text/text.component.ts
@@ -1,33 +1,42 @@
 // Copyright (c) 2019 the Octant contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 //
-import { Component } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import { TextView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-text',
   templateUrl: './text.component.html',
   styleUrls: ['./text.component.scss'],
 })
-export class TextComponent extends AbstractViewComponent<TextView> {
+export class TextComponent implements OnChanges {
+  v: TextView;
+
+  @Input() set view(v: TextView) {
+    this.v = v as TextView;
+  }
+
+  get view(): TextView {
+    return this.v;
+  }
+
   value: string;
 
   isMarkdown: boolean;
 
   hasStatus = false;
 
-  constructor() {
-    super();
-  }
+  constructor() {}
 
-  update() {
-    const view = this.v;
-    this.value = view.config.value;
-    this.isMarkdown = view.config.isMarkdown;
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view && changes.view.currentValue) {
+      const view = changes.view.currentValue as TextView;
+      this.value = view.config.value;
+      this.isMarkdown = view.config.isMarkdown;
 
-    if (view.config.status) {
-      this.hasStatus = true;
+      if (view.config.status) {
+        this.hasStatus = true;
+      }
     }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.scss b/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.scss
index f834d136..4f6d645d 100644
--- a/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.scss
+++ b/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.scss
@@ -4,5 +4,5 @@
 @import '../../../../../../styles';
 
 .tooltip-wrapper {
-  margin: 0 0.3rem 0 0;
+  margin: 0 .3rem 0 0;
 }
diff --git a/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.ts b/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.ts
index 04725051..17ead553 100644
--- a/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.ts
+++ b/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.ts
@@ -2,22 +2,39 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import {
+  Component,
+  Input,
+  OnChanges,
+  OnDestroy,
+  SimpleChanges,
+  ChangeDetectionStrategy,
+  OnInit,
+  ChangeDetectorRef,
+} from '@angular/core';
 import dayjs from 'dayjs';
 import utc from 'dayjs/plugin/utc';
 import LocalizedFormat from 'dayjs/plugin/localizedFormat';
-import { TimestampView } from 'src/app/modules/shared/models/content';
+import { TimestampView, View } from 'src/app/modules/shared/models/content';
 import { Subscription } from 'rxjs';
 import { ContentService } from '../../../services/content/content.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-view-timestamp',
   templateUrl: './timestamp.component.html',
   styleUrls: ['./timestamp.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class TimestampComponent extends AbstractViewComponent<TimestampView>
-  implements OnInit, OnDestroy {
+export class TimestampComponent implements OnInit, OnChanges, OnDestroy {
+  private v: TimestampView;
+
+  @Input() set view(v: View) {
+    this.v = v as TimestampView;
+  }
+  get view() {
+    return this.v;
+  }
+
   timestamp: number;
   humanReadable: string;
   age: string;
@@ -28,7 +45,6 @@ export class TimestampComponent extends AbstractViewComponent<TimestampView>
     private contentService: ContentService,
     private cd: ChangeDetectorRef
   ) {
-    super();
     dayjs.extend(utc);
     dayjs.extend(LocalizedFormat);
   }
@@ -46,12 +62,16 @@ export class TimestampComponent extends AbstractViewComponent<TimestampView>
     return `${-this.scrollPosition - 64}px`;
   }
 
-  update() {
-    this.timestamp = this.v.config.timestamp;
-    this.humanReadable =
-      dayjs(this.timestamp * 1000)
-        .utc()
-        .format('llll') + ' UTC';
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view && changes.view.currentValue) {
+      const view = changes.view.currentValue as TimestampView;
+
+      this.timestamp = view.config.timestamp;
+      this.humanReadable =
+        dayjs(this.timestamp * 1000)
+          .utc()
+          .format('llll') + ' UTC';
+    }
   }
 
   ngOnDestroy(): void {
diff --git a/web/src/app/modules/shared/components/presentation/yaml/yaml.component.ts b/web/src/app/modules/shared/components/presentation/yaml/yaml.component.ts
index 6dc84f9a..3fcd86ed 100644
--- a/web/src/app/modules/shared/components/presentation/yaml/yaml.component.ts
+++ b/web/src/app/modules/shared/components/presentation/yaml/yaml.component.ts
@@ -2,23 +2,32 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component } from '@angular/core';
-import { YAMLView } from 'src/app/modules/shared/models/content';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { View, YAMLView } from 'src/app/modules/shared/models/content';
 
 @Component({
   selector: 'app-view-yaml',
   templateUrl: './yaml.component.html',
   styleUrls: ['./yaml.component.scss'],
 })
-export class YamlComponent extends AbstractViewComponent<YAMLView> {
-  source: string;
+export class YamlComponent implements OnChanges {
+  private v: YAMLView;
 
-  constructor() {
-    super();
+  @Input() set view(v: View) {
+    this.v = v as YAMLView;
+  }
+  get view() {
+    return this.v;
   }
 
-  update() {
-    this.source = this.v.config.data;
+  source: string;
+
+  constructor() {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as YAMLView;
+      this.source = view.config.data;
+    }
   }
 }
diff --git a/web/src/app/modules/shared/components/smart/context-selector/context-selector.component.html b/web/src/app/modules/shared/components/smart/context-selector/context-selector.component.html
index fd3ebecf..3405c5e2 100644
--- a/web/src/app/modules/shared/components/smart/context-selector/context-selector.component.html
+++ b/web/src/app/modules/shared/components/smart/context-selector/context-selector.component.html
@@ -2,17 +2,13 @@
 </ng-container>
 
 <ng-template #contextMenu>
-  <clr-dropdown class="dropdown-top" [clrCloseMenuOnItemClick]="true">
+  <clr-dropdown class= "dropdown-top" [clrCloseMenuOnItemClick]="true">
     <button class="dropdown-button" type="button" clrDropdownTrigger>
       <clr-icon shape="cluster"></clr-icon>
       {{ selected | truncate }}
       <clr-icon shape="caret down"></clr-icon>
     </button>
-    <clr-dropdown-menu
-      class="dropdown-top"
-      *clrIfOpen
-      [clrPosition]="'bottom-right'"
-    >
+    <clr-dropdown-menu class="dropdown-top" *clrIfOpen [clrPosition]="'bottom-right'">
       <label class="dropdown-header">Contexts</label>
 
       <ng-container *ngFor="let context of contexts; trackBy: trackByFn">
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 26772894..246aa3cb 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
@@ -27,5 +27,5 @@
 }
 
 .dropdown .dropdown-toggle:not(.btn) {
-  padding: 0 1rem 0 0;
+  padding: 0 1.0rem 0 0;
 }
diff --git a/web/src/app/modules/shared/components/smart/editor/editor.component.html b/web/src/app/modules/shared/components/smart/editor/editor.component.html
index ea884f02..90a76606 100644
--- a/web/src/app/modules/shared/components/smart/editor/editor.component.html
+++ b/web/src/app/modules/shared/components/smart/editor/editor.component.html
@@ -1,22 +1,15 @@
 <div class="editor-container">
-  <div class="editor">
-    <ng-monaco-editor
-      [(ngModel)]="value"
-      [options]="options"
-      modelUri="file:text.yaml"
-    ></ng-monaco-editor>
-  </div>
+    <div class="editor">
+        <ng-monaco-editor
+                [(ngModel)]="value"
+                [options]="options"
+                modelUri="file:text.yaml"
+        ></ng-monaco-editor>
+    </div>
+
+    <div class="controls">
+        <button (click)="reset()" [disabled]="isUpdateEnabled()" class="btn">Reset</button>
+        <button (click)="submit()" [disabled]="isUpdateEnabled()" class="btn btn-primary">{{ submitLabel }}</button>
+    </div>
 
-  <div class="controls">
-    <button (click)="reset()" [disabled]="isUpdateEnabled()" class="btn">
-      Reset
-    </button>
-    <button
-      (click)="submit()"
-      [disabled]="isUpdateEnabled()"
-      class="btn btn-primary"
-    >
-      {{ submitLabel }}
-    </button>
-  </div>
 </div>
diff --git a/web/src/app/modules/shared/components/smart/editor/editor.component.ts b/web/src/app/modules/shared/components/smart/editor/editor.component.ts
index 70c174fa..52a79a45 100644
--- a/web/src/app/modules/shared/components/smart/editor/editor.component.ts
+++ b/web/src/app/modules/shared/components/smart/editor/editor.component.ts
@@ -3,11 +3,10 @@ Copyright (c) 2020 the Octant contributors. All Rights Reserved.
 SPDX-License-Identifier: Apache-2.0
 */
 
-import { Component } from '@angular/core';
-import { EditorView } from '../../../models/content';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { EditorView, View } from '../../../models/content';
 import { NamespaceService } from '../../../services/namespace/namespace.service';
 import { ActionService } from '../../../services/action/action.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 interface Options {
   readOnly: boolean;
@@ -19,7 +18,17 @@ interface Options {
   templateUrl: './editor.component.html',
   styleUrls: ['./editor.component.scss'],
 })
-export class EditorComponent extends AbstractViewComponent<EditorView> {
+export class EditorComponent implements OnChanges {
+  private v: EditorView;
+
+  @Input() set view(v: View) {
+    this.v = v as EditorView;
+  }
+
+  get view() {
+    return this.v;
+  }
+
   set value(v: string) {
     if (v !== this.editorValue) {
       this.isModified = true;
@@ -37,7 +46,7 @@ export class EditorComponent extends AbstractViewComponent<EditorView> {
 
   isModified = false;
 
-  options: Options = { language: 'yaml', readOnly: false };
+  options: Options;
 
   submitAction = 'action.octant.dev/update';
   submitLabel = 'Update';
@@ -46,22 +55,24 @@ export class EditorComponent extends AbstractViewComponent<EditorView> {
     private namespaceService: NamespaceService,
     private actionService: ActionService
   ) {
-    super();
+    this.options = {} as Options;
   }
 
-  update() {
-    const view = this.v;
+  ngOnChanges(changes: SimpleChanges) {
+    if (changes.view.currentValue) {
+      const view = changes.view.currentValue as EditorView;
 
-    if (!this.isModified) {
-      this.editorValue = view.config.value;
-      this.metadata = view.config.metadata;
-      this.pristineValue = view.config.value;
-      this.options.readOnly = view.config.readOnly;
-      this.options.language = view.config.language;
-    }
+      if (!this.isModified) {
+        this.editorValue = view.config.value;
+        this.metadata = view.config.metadata;
+        this.pristineValue = view.config.value;
+        this.options.readOnly = view.config.readOnly;
+        this.options.language = view.config.language;
+      }
 
-    this.submitAction = view.config.submitAction || this.submitAction;
-    this.submitLabel = view.config.submitLabel || this.submitLabel;
+      this.submitAction = view.config.submitAction || this.submitAction;
+      this.submitLabel = view.config.submitLabel || this.submitLabel;
+    }
   }
 
   submit() {
diff --git a/web/src/app/modules/shared/components/smart/helper/helper.component.html b/web/src/app/modules/shared/components/smart/helper/helper.component.html
index ae03c8c2..c14083c9 100644
--- a/web/src/app/modules/shared/components/smart/helper/helper.component.html
+++ b/web/src/app/modules/shared/components/smart/helper/helper.component.html
@@ -4,13 +4,7 @@
       <clr-icon shape="help"></clr-icon>
     </button>
     <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
-      <div
-        aria-label="Dropdown header About Octant"
-        clrDropdownItem
-        (click)="isModalOpen = true"
-      >
-        About Octant
-      </div>
+      <div aria-label="Dropdown header About Octant" clrDropdownItem (click)="isModalOpen = true">About Octant</div>
     </clr-dropdown-menu>
   </clr-dropdown>
 </div>
@@ -21,23 +15,21 @@
     <table class="table table-vertical table-noborder">
       <tbody>
         <tr>
-          <th>Version</th>
-          <td>{{ version }}</td>
+            <th>Version</th>
+            <td>{{ version }}</td>
         </tr>
         <tr>
-          <th>Commit</th>
-          <td>{{ commit }}</td>
+            <th>Commit</th>
+            <td>{{ commit }}</td>
         </tr>
         <tr>
-          <th>Built</th>
-          <td>{{ time }}</td>
+            <th>Built</th>
+            <td>{{ time }}</td>
         </tr>
       </tbody>
     </table>
   </div>
   <div class="modal-footer">
-    <button type="button" class="btn btn-primary" (click)="isModalOpen = false">
-      Close
-    </button>
+    <button type="button" class="btn btn-primary" (click)="isModalOpen = false">Close</button>
   </div>
-</clr-modal>
+</clr-modal>
\ No newline at end of file
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.html b/web/src/app/modules/shared/components/smart/logs/logs.component.html
index 2f68e119..85ce93cf 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.html
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.html
@@ -11,8 +11,7 @@
         <option
           *ngFor="let container of v?.config.containers"
           value="{{ container }}"
-        >
-          {{ container === '' ? '[all containers]' : container }}</option
+        > {{ container === '' ? '[all containers]' : container }}</option
         >
       </select>
     </clr-select-container>
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.integration.spec.ts b/web/src/app/modules/shared/components/smart/logs/logs.component.integration.spec.ts
index 18f7158d..eb7becd6 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.integration.spec.ts
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.integration.spec.ts
@@ -74,15 +74,35 @@ describe('LogsComponent <-> PodsLogsService', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(LogsComponent);
     component = fixture.componentInstance;
+    fixture.detectChanges();
     service = TestBed.inject(PodLogsService);
     httpClient = TestBed.inject(HttpClient);
     httpTestingController = TestBed.inject(HttpTestingController);
+  });
+
+  it('should render list of containers to choose from', () => {
+    expect(component.selectedContainer).toBe('');
 
     component.view = createTestLogsView([
       'containerA',
       'containerB',
       'containerC',
     ]);
+    fixture.detectChanges();
+
+    const selectOptionsDebugElements: DebugElement[] = fixture.debugElement.queryAll(
+      By.css('.container-select select > option')
+    );
+    expect(selectOptionsDebugElements.length).toBe(3);
+    expect(selectOptionsDebugElements[0].nativeElement.value).toBe(
+      'containerA'
+    );
+    expect(selectOptionsDebugElements[1].nativeElement.value).toBe(
+      'containerB'
+    );
+    expect(selectOptionsDebugElements[2].nativeElement.value).toBe(
+      'containerC'
+    );
   });
 
   it('should allow user to toggle displaying timestamps', () => {
@@ -122,7 +142,7 @@ describe('LogsComponent <-> PodsLogsService', () => {
     );
   });
 
-  it('should continuously scroll to new logs if user has already scrolled to the bottom', () => {
+  it('should continously scroll to new logs if user has already scrolled to the bottom', () => {
     const numberOfEntriesRequiredToScroll = 200;
     component.containerLogs = map(
       range(numberOfEntriesRequiredToScroll),
@@ -203,13 +223,7 @@ describe('LogsComponent <-> PodsLogsService', () => {
     expect(logWrapperNativeElement.scrollTop).toBe(halfwayScrollMark);
   });
 
-  it('should filter messages based on search string', () => {
-    component.view = createTestLogsView([
-      'containerA',
-      'containerB',
-      'containerC',
-    ]);
-
+  it('should filer messages based on search string', () => {
     component.shouldDisplayTimestamp = true;
     component.containerLogs = defaultTestLogs;
     component.filterText = 'message';
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.spec.ts b/web/src/app/modules/shared/components/smart/logs/logs.component.spec.ts
index 462d598d..790b6983 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.spec.ts
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.spec.ts
@@ -4,7 +4,7 @@
 
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 import { LogsComponent } from './logs.component';
-import { LogEntry, LogsView } from 'src/app/modules/shared/models/content';
+import { LogEntry } from 'src/app/modules/shared/models/content';
 import { By } from '@angular/platform-browser';
 import { DebugElement } from '@angular/core';
 import { AnsiPipe } from '../../../pipes/ansiPipe/ansi.pipe';
@@ -40,19 +40,6 @@ describe('LogsComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(LogsComponent);
     component = fixture.componentInstance;
-    component.view = {
-      metadata: {
-        type: 'logs',
-        title: [],
-        accessor: 'logs',
-      },
-      config: {
-        namespace: 'default',
-        name: 'cartpod',
-        containers: [],
-      },
-    } as LogsView;
-
     fixture.detectChanges();
   });
 
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.ts b/web/src/app/modules/shared/components/smart/logs/logs.component.ts
index 406f49f9..0db2f7dd 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.ts
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.ts
@@ -5,10 +5,9 @@
 import {
   AfterContentChecked,
   AfterViewChecked,
-  ChangeDetectionStrategy,
-  ChangeDetectorRef,
   Component,
   ElementRef,
+  Input,
   IterableDiffer,
   IterableDiffers,
   OnDestroy,
@@ -16,24 +15,35 @@ import {
   ViewChild,
   ViewEncapsulation,
 } from '@angular/core';
-import { LogEntry, LogsView } from 'src/app/modules/shared/models/content';
+import {
+  LogEntry,
+  LogsView,
+  View,
+} from 'src/app/modules/shared/models/content';
 import {
   PodLogsService,
   PodLogsStreamer,
 } from 'src/app/modules/shared/pod-logs/pod-logs.service';
 import { formatDate } from '@angular/common';
 import { Subscription } from 'rxjs';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-logs',
   templateUrl: './logs.component.html',
   styleUrls: ['./logs.component.scss'],
   encapsulation: ViewEncapsulation.None,
-  changeDetection: ChangeDetectionStrategy.Default,
 })
-export class LogsComponent extends AbstractViewComponent<LogsView>
+export class LogsComponent
   implements OnInit, OnDestroy, AfterContentChecked, AfterViewChecked {
+  v: LogsView;
+
+  @Input() set view(v: View) {
+    this.v = v as LogsView;
+  }
+  get view() {
+    return this.v;
+  }
+
   private logStream: PodLogsStreamer;
   scrollToBottom = true;
 
@@ -56,22 +66,18 @@ export class LogsComponent extends AbstractViewComponent<LogsView>
 
   constructor(
     private podLogsService: PodLogsService,
-    private iterableDiffers: IterableDiffers,
-    private cdr: ChangeDetectorRef
-  ) {
-    super();
-  }
+    private iterableDiffers: IterableDiffers
+  ) {}
 
   ngOnInit() {
     this.containerLogsDiffer = this.iterableDiffers
       .find(this.containerLogs)
       .create();
-    this.startStream();
-  }
-
-  protected update() {
-    if (this.v.config.containers && this.v.config.containers.length > 0) {
-      this.selectedContainer = this.v.config.containers[0];
+    if (this.v) {
+      if (this.v.config.containers && this.v.config.containers.length > 0) {
+        this.selectedContainer = this.v.config.containers[0];
+      }
+      this.startStream();
     }
   }
 
@@ -119,7 +125,6 @@ export class LogsComponent extends AbstractViewComponent<LogsView>
           }
           this.containerLogs.push(entry);
           this.updateSelectedCount();
-          this.cdr.markForCheck();
         }
       );
     }
diff --git a/web/src/app/modules/shared/components/smart/slider-view/slider-view.component.html b/web/src/app/modules/shared/components/smart/slider-view/slider-view.component.html
index 8881a0cb..7828811e 100644
--- a/web/src/app/modules/shared/components/smart/slider-view/slider-view.component.html
+++ b/web/src/app/modules/shared/components/smart/slider-view/slider-view.component.html
@@ -16,11 +16,13 @@ SPDX-License-Identifier: Apache-2.0 -->
     <clr-icon shape="angle" [class]="animationState"></clr-icon>
   </a>
   <div class="status-view--content" [ngStyle]="contentStyle">
-    <app-object-tabs
-      [views]="tabs"
-      [payloads]="payloads"
-      [closable]="true"
-      [extView]="true"
-    ></app-object-tabs>
+    <ng-container *ngIf="view?.config.tabs">
+      <app-object-tabs
+        [views]="tabs"
+        [payloads]="payloads"
+        [closable]="true"
+        [extView]="true"
+      ></app-object-tabs>
+    </ng-container>
   </div>
 </div>
diff --git a/web/src/app/modules/shared/components/smart/slider-view/slider-view.component.ts b/web/src/app/modules/shared/components/smart/slider-view/slider-view.component.ts
index 87a9d8a2..87106552 100644
--- a/web/src/app/modules/shared/components/smart/slider-view/slider-view.component.ts
+++ b/web/src/app/modules/shared/components/smart/slider-view/slider-view.component.ts
@@ -3,12 +3,11 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import { Component, OnChanges, SimpleChanges } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import { ExtensionView, View } from 'src/app/modules/shared/models/content';
 import { SlideInOutAnimation } from './slide-in-out.animation';
 import { ResizeEvent } from 'angular-resizable-element';
 import { SliderService } from 'src/app/modules/shared/slider/slider.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   selector: 'app-slider-view',
@@ -16,8 +15,9 @@ import { AbstractViewComponent } from '../../abstract-view/abstract-view.compone
   styleUrls: ['./slider-view.component.scss'],
   animations: [SlideInOutAnimation],
 })
-export class SliderViewComponent extends AbstractViewComponent<ExtensionView>
-  implements OnChanges {
+export class SliderViewComponent implements OnChanges {
+  @Input() view: ExtensionView;
+
   style: object = {};
   contentStyle: object = {};
   animationState = 'out';
@@ -26,43 +26,33 @@ export class SliderViewComponent extends AbstractViewComponent<ExtensionView>
   tabs: View[] = [];
   payloads: { [key: string]: string }[] = [];
 
-  constructor(private sliderService: SliderService) {
-    super();
-  }
-
-  update() {
-    const extView = this.v;
-    if (extView && extView.config.tabs) {
-      this.tabs = [];
-      this.payloads = [];
-      extView.config.tabs.forEach(tab => {
-        this.tabs.push(tab.tab);
-        this.payloads.push(tab.payload);
-      });
-    } else {
-      this.animationState = 'out';
-      this.sliderService.resetDefault();
-    }
-  }
+  constructor(private sliderService: SliderService) {}
 
   ngOnChanges(changes: SimpleChanges): void {
     if (changes.view.currentValue) {
-      if (!changes.view.isFirstChange() && this.animationState === 'out') {
-        const prevExtView = changes.view.previousValue as ExtensionView;
-        if (
-          prevExtView.config.tabs === null &&
-          changes.view.currentValue.config.tabs.length
-        ) {
-          this.slide();
-          return;
-        }
-        if (
-          changes.view.currentValue.config.tabs.length >
-          prevExtView.config.tabs.length
-        ) {
-          this.slide();
-          return;
+      const extView = changes.view.currentValue as ExtensionView;
+      if (extView.config.tabs) {
+        this.tabs = [];
+        this.payloads = [];
+        extView.config.tabs.forEach(tab => {
+          this.tabs.push(tab.tab);
+          this.payloads.push(tab.payload);
+        });
+
+        if (!changes.view.isFirstChange() && this.animationState === 'out') {
+          const prevExtView = changes.view.previousValue as ExtensionView;
+          if (prevExtView.config.tabs === null && extView.config.tabs.length) {
+            this.slide();
+            return;
+          }
+          if (extView.config.tabs.length > prevExtView.config.tabs.length) {
+            this.slide();
+            return;
+          }
         }
+      } else {
+        this.animationState = 'out';
+        this.sliderService.resetDefault();
       }
     }
   }
diff --git a/web/src/app/modules/shared/components/smart/terminal/terminal.component.html b/web/src/app/modules/shared/components/smart/terminal/terminal.component.html
index 2da6ad13..eea0b9aa 100644
--- a/web/src/app/modules/shared/components/smart/terminal/terminal.component.html
+++ b/web/src/app/modules/shared/components/smart/terminal/terminal.component.html
@@ -8,7 +8,10 @@
       (change)="onContainerChange($event.target.value)"
     >
       <option
-        *ngFor="let container of containers; trackBy: trackByIdentity"
+        *ngFor="
+          let container of view?.config.containers;
+          trackBy: trackByIdentity
+        "
         value="{{ container }}"
         >{{ container }}</option
       >
diff --git a/web/src/app/modules/shared/components/smart/terminal/terminal.component.spec.ts b/web/src/app/modules/shared/components/smart/terminal/terminal.component.spec.ts
index 658050da..7e1ed4de 100644
--- a/web/src/app/modules/shared/components/smart/terminal/terminal.component.spec.ts
+++ b/web/src/app/modules/shared/components/smart/terminal/terminal.component.spec.ts
@@ -3,7 +3,6 @@
 //
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 import { TerminalComponent } from './terminal.component';
-import { TerminalView } from '../../../models/content';
 
 describe('TerminalComponent', () => {
   let component: TerminalComponent;
@@ -18,20 +17,6 @@ describe('TerminalComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(TerminalComponent);
     component = fixture.componentInstance;
-    component.view = {
-      metadata: {
-        type: '',
-      },
-      config: {
-        containers: [],
-        name: 'name',
-        namespace: 'namespace',
-        podName: 'pod-name',
-        terminal: {
-          active: false,
-        },
-      },
-    } as TerminalView;
     fixture.detectChanges();
   });
 
diff --git a/web/src/app/modules/shared/components/smart/terminal/terminal.component.ts b/web/src/app/modules/shared/components/smart/terminal/terminal.component.ts
index f9032b53..934bf4df 100644
--- a/web/src/app/modules/shared/components/smart/terminal/terminal.component.ts
+++ b/web/src/app/modules/shared/components/smart/terminal/terminal.component.ts
@@ -3,9 +3,10 @@
 //
 import {
   AfterViewInit,
-  ChangeDetectionStrategy,
   Component,
   ElementRef,
+  HostListener,
+  Input,
   OnDestroy,
   ViewChild,
   ViewEncapsulation,
@@ -19,40 +20,26 @@ import {
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
 import { TerminalView } from 'src/app/modules/shared/models/content';
 import { WebsocketService } from '../../../services/websocket/websocket.service';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
 @Component({
   encapsulation: ViewEncapsulation.None,
   selector: 'app-terminal',
   styleUrls: ['./terminal.component.scss'],
   templateUrl: './terminal.component.html',
-  changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class TerminalComponent extends AbstractViewComponent<TerminalView>
-  implements OnDestroy, AfterViewInit {
-  @ViewChild('terminal', { static: true }) terminalDiv: ElementRef;
+export class TerminalComponent implements OnDestroy, AfterViewInit {
+  constructor(
+    private terminalService: TerminalOutputService,
+    private wss: WebsocketService
+  ) {}
 
   selectedContainer = '';
-  containers: string[] = [];
-
   private terminalStream: TerminalOutputStreamer;
   private term: Terminal;
   private fitAddon: FitAddon;
-
   trackByIdentity = trackByIdentity;
-
-  constructor(
-    private terminalService: TerminalOutputService,
-    private wss: WebsocketService
-  ) {
-    super();
-  }
-
-  update() {
-    if (this.v) {
-      this.containers = this.v.config.containers;
-    }
-  }
+  @Input() view: TerminalView;
+  @ViewChild('terminal', { static: true }) terminalDiv: ElementRef;
 
   ngOnDestroy(): void {
     if (this.terminalStream) {
@@ -64,32 +51,32 @@ export class TerminalComponent extends AbstractViewComponent<TerminalView>
   }
 
   ngAfterViewInit() {
-    const logLevel = 'info';
-    const { terminal } = this.v.config;
-    const { active } = terminal;
-    const disableStdin = !active;
-    this.term = new Terminal({
-      logLevel,
-      disableStdin,
-    });
-    setTimeout(() => {
-      this.initStream();
-    });
-    this.enableResize();
-    this.term.onData(data => {
-      if (active) {
-        this.wss.sendMessage('action.octant.dev/sendTerminalCommand', {
-          key: data,
-        });
-      }
-    });
-    this.fitAddon = new FitAddon();
-    this.term.loadAddon(this.fitAddon);
-    this.term.open(this.terminalDiv.nativeElement);
-    this.term.focus();
-    this.fitAddon.fit();
-
-    super.ngAfterViewInit();
+    if (this.view) {
+      const logLevel = 'info';
+      const { terminal } = this.view.config;
+      const { active } = terminal;
+      const disableStdin = !active;
+      this.term = new Terminal({
+        logLevel,
+        disableStdin,
+      });
+      setTimeout(() => {
+        this.initStream();
+      });
+      this.enableResize();
+      this.term.onData(data => {
+        if (active) {
+          this.wss.sendMessage('action.octant.dev/sendTerminalCommand', {
+            key: data,
+          });
+        }
+      });
+      this.fitAddon = new FitAddon();
+      this.term.loadAddon(this.fitAddon);
+      this.term.open(this.terminalDiv.nativeElement);
+      this.term.focus();
+      this.fitAddon.fit();
+    }
   }
 
   enableResize() {
@@ -121,7 +108,7 @@ export class TerminalComponent extends AbstractViewComponent<TerminalView>
   }
 
   initStream() {
-    const { namespace, podName, terminal } = this.v.config;
+    const { namespace, podName, terminal } = this.view.config;
     const { container } = terminal;
     if (this.terminalService.selectedContainer) {
       this.selectedContainer = this.terminalService.selectedContainer;
diff --git a/web/src/app/modules/shared/components/view/view-container.component.spec.ts b/web/src/app/modules/shared/components/view/view-container.component.spec.ts
deleted file mode 100644
index 60e87308..00000000
--- a/web/src/app/modules/shared/components/view/view-container.component.spec.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { ViewContainerComponent } from './view-container.component';
-import { DYNAMIC_COMPONENTS_MAPPING } from '../../dynamic-components';
-import { TextComponent } from '../presentation/text/text.component';
-
-describe('ViewContainerComponent', () => {
-  let component: ViewContainerComponent;
-  let fixture: ComponentFixture<ViewContainerComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ViewContainerComponent],
-      providers: [
-        {
-          provide: DYNAMIC_COMPONENTS_MAPPING,
-          useValue: {
-            text: TextComponent,
-          },
-        },
-      ],
-    }).compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(ViewContainerComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/web/src/app/modules/shared/components/view/view-container.component.ts b/web/src/app/modules/shared/components/view/view-container.component.ts
deleted file mode 100644
index fcccb20c..00000000
--- a/web/src/app/modules/shared/components/view/view-container.component.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import {
-  AfterViewInit,
-  ChangeDetectionStrategy,
-  Component,
-  ComponentFactoryResolver,
-  EventEmitter,
-  Inject,
-  Input,
-  OnChanges,
-  OnInit,
-  Output,
-  SimpleChanges,
-  Type,
-  ViewChild,
-} from '@angular/core';
-import { View } from '../../models/content';
-import { ViewHostDirective } from '../../directives/view-host/view-host.directive';
-import {
-  ComponentMapping,
-  DYNAMIC_COMPONENTS_MAPPING,
-} from '../../dynamic-components';
-import { MissingComponentComponent } from '../missing-component/missing-component.component';
-
-interface Viewer {
-  view: View;
-  viewInit: EventEmitter<void>;
-  ping: () => void;
-}
-
-@Component({
-  selector: 'app-view-container',
-  template: `<ng-container appView></ng-container>`,
-  changeDetection: ChangeDetectionStrategy.OnPush,
-})
-export class ViewContainerComponent
-  implements OnInit, OnChanges, AfterViewInit {
-  @ViewChild(ViewHostDirective, { static: true }) appView: ViewHostDirective;
-  @Input() view: View;
-  @Input() enableDebug = false;
-  @Output() viewInit: EventEmitter<void> = new EventEmitter<void>();
-
-  private start: number;
-
-  constructor(
-    private componentFactoryResolver: ComponentFactoryResolver,
-    @Inject(DYNAMIC_COMPONENTS_MAPPING)
-    private componentMappings: ComponentMapping
-  ) {}
-
-  ngOnInit(): void {
-    this.loadView();
-
-    if (this.enableDebug) {
-      this.start = new Date().getTime();
-    }
-  }
-
-  ngAfterViewInit() {
-    if (this.view === null) {
-      return;
-    }
-
-    if (this.enableDebug) {
-      console.log(
-        `${this.view.metadata.type}: ${new Date().getTime() - this.start}`
-      );
-    }
-  }
-
-  ngOnChanges(changes: SimpleChanges) {
-    if (changes.view.currentValue) {
-      // TODO: send a checksum so this doesn't need to be calculated here.
-      const prev = JSON.stringify(changes.view.previousValue);
-      const cur = JSON.stringify(changes.view.currentValue);
-
-      if (cur !== prev) {
-        this.loadView();
-      }
-    }
-  }
-
-  loadView() {
-    if (!this.view || !this.view.metadata) {
-      return;
-    }
-
-    const viewType = this.view.metadata.type;
-    let component: Type<any> = this.componentMappings[viewType];
-    if (!component) {
-      component = MissingComponentComponent;
-    }
-
-    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
-      component
-    );
-    const viewContainerRef = this.appView.viewContainerRef;
-    viewContainerRef.clear();
-
-    const componentRef = viewContainerRef.createComponent<Viewer>(
-      componentFactory
-    );
-    componentRef.instance.view = this.view;
-    componentRef.instance.viewInit.subscribe(_ => this.viewInit.emit());
-  }
-}
diff --git a/web/src/app/modules/shared/directives/view-host/view-host.directive.spec.ts b/web/src/app/modules/shared/directives/view-host/view-host.directive.spec.ts
deleted file mode 100644
index 9b58ca55..00000000
--- a/web/src/app/modules/shared/directives/view-host/view-host.directive.spec.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { ViewHostDirective } from './view-host.directive';
-
-describe('ViewHostDirective', () => {
-  it('should create an instance', () => {
-    const directive = new ViewHostDirective(undefined);
-    expect(directive).toBeTruthy();
-  });
-});
diff --git a/web/src/app/modules/shared/directives/view-host/view-host.directive.ts b/web/src/app/modules/shared/directives/view-host/view-host.directive.ts
deleted file mode 100644
index 06ab3b00..00000000
--- a/web/src/app/modules/shared/directives/view-host/view-host.directive.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { Directive, ViewContainerRef } from '@angular/core';
-
-@Directive({
-  selector: '[appView]',
-})
-export class ViewHostDirective {
-  constructor(public viewContainerRef: ViewContainerRef) {}
-}
diff --git a/web/src/app/modules/shared/dynamic-components.ts b/web/src/app/modules/shared/dynamic-components.ts
deleted file mode 100644
index b1af6c26..00000000
--- a/web/src/app/modules/shared/dynamic-components.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { InjectionToken, Type } from '@angular/core';
-import { AnnotationsComponent } from './components/presentation/annotations/annotations.component';
-import { IFrameComponent } from './components/presentation/iframe/iframe.component';
-import { ResourceViewerComponent } from './components/presentation/resource-viewer/resource-viewer.component';
-import { LogsComponent } from './components/smart/logs/logs.component';
-import { PodStatusComponent } from './components/presentation/pod-status/pod-status.component';
-import { ButtonGroupComponent } from './components/presentation/button-group/button-group.component';
-import { CardComponent } from './components/presentation/card/card.component';
-import { YamlComponent } from './components/presentation/yaml/yaml.component';
-import { CodeComponent } from './components/presentation/code/code.component';
-import { EditorComponent } from './components/smart/editor/editor.component';
-import { PortForwardComponent } from './components/presentation/port-forward/port-forward.component';
-import { ExpressionSelectorComponent } from './components/presentation/expression-selector/expression-selector.component';
-import { ListComponent } from './components/presentation/list/list.component';
-import { FlexlayoutComponent } from './components/presentation/flexlayout/flexlayout.component';
-import { LoadingComponent } from './components/presentation/loading/loading.component';
-import { TextComponent } from './components/presentation/text/text.component';
-import { QuadrantComponent } from './components/presentation/quadrant/quadrant.component';
-import { SelectorsComponent } from './components/presentation/selectors/selectors.component';
-import { TerminalComponent } from './components/smart/terminal/terminal.component';
-import { DatagridComponent } from './components/presentation/datagrid/datagrid.component';
-import { DonutChartComponent } from './components/presentation/donut-chart/donut-chart.component';
-import { GraphvizComponent } from './components/presentation/graphviz/graphviz.component';
-import { ErrorComponent } from './components/presentation/error/error.component';
-import { LabelSelectorComponent } from './components/presentation/label-selector/label-selector.component';
-import { PortsComponent } from './components/presentation/ports/ports.component';
-import { CardListComponent } from './components/presentation/card-list/card-list.component';
-import { LabelsComponent } from './components/presentation/labels/labels.component';
-import { LinkComponent } from './components/presentation/link/link.component';
-import { SingleStatComponent } from './components/presentation/single-stat/single-stat.component';
-import { SummaryComponent } from './components/presentation/summary/summary.component';
-import { StepperComponent } from './components/presentation/stepper/stepper.component';
-import { TimestampComponent } from './components/presentation/timestamp/timestamp.component';
-import { ContainersComponent } from './components/presentation/containers/containers.component';
-
-export interface ComponentMapping {
-  [key: string]: Type<any>;
-}
-
-const DynamicComponentMapping: ComponentMapping = {
-  annotations: AnnotationsComponent,
-  buttonGroup: ButtonGroupComponent,
-  card: CardComponent,
-  cardList: CardListComponent,
-  codeBlock: CodeComponent,
-  containers: ContainersComponent,
-  donutChart: DonutChartComponent,
-  editor: EditorComponent,
-  expressionSelector: ExpressionSelectorComponent,
-  graphviz: GraphvizComponent,
-  flexlayout: FlexlayoutComponent,
-  labels: LabelsComponent,
-  labelSelector: LabelSelectorComponent,
-  loading: LoadingComponent,
-  error: ErrorComponent,
-  iframe: IFrameComponent,
-  link: LinkComponent,
-  list: ListComponent,
-  logs: LogsComponent,
-  podStatus: PodStatusComponent,
-  portforward: PortForwardComponent,
-  ports: PortsComponent,
-  quadrant: QuadrantComponent,
-  resourceViewer: ResourceViewerComponent,
-  selectors: SelectorsComponent,
-  singleStat: SingleStatComponent,
-  stepper: StepperComponent,
-  summary: SummaryComponent,
-  table: DatagridComponent,
-  terminal: TerminalComponent,
-  text: TextComponent,
-  timestamp: TimestampComponent,
-  yaml: YamlComponent,
-};
-
-export const DYNAMIC_COMPONENTS_MAPPING = new InjectionToken<ComponentMapping>(
-  'dynamicComponentsMapping'
-);
-
-export const dynamicComponents = () => {
-  return {
-    provide: DYNAMIC_COMPONENTS_MAPPING,
-    useValue: DynamicComponentMapping,
-  };
-};
diff --git a/web/src/app/modules/shared/highlight.ts b/web/src/app/modules/shared/highlight.ts
index 66b6ee3f..9e9e6959 100644
--- a/web/src/app/modules/shared/highlight.ts
+++ b/web/src/app/modules/shared/highlight.ts
@@ -6,16 +6,18 @@
 
 import { HIGHLIGHT_OPTIONS } from 'ngx-highlightjs';
 
-const languages = () => {
+export function hljsLanguages() {
   return {
     json: () => import('highlight.js/lib/languages/json'),
     yaml: () => import('highlight.js/lib/languages/yaml'),
   };
-};
+}
 
-export const highlightProvider = () => ({
-  provide: HIGHLIGHT_OPTIONS,
-  useValue: {
-    languages: languages(),
-  },
-});
+export function highlightProvider() {
+  return {
+    provide: HIGHLIGHT_OPTIONS,
+    useValue: {
+      languages: hljsLanguages(),
+    },
+  };
+}
diff --git a/web/src/app/modules/shared/models/content.ts b/web/src/app/modules/shared/models/content.ts
index 88d542cf..5b1853e3 100644
--- a/web/src/app/modules/shared/models/content.ts
+++ b/web/src/app/modules/shared/models/content.ts
@@ -243,7 +243,7 @@ export interface Node {
   apiVersion: string;
   kind: string;
   status: string;
-  details: View[];
+  details: View;
   path: LinkView;
 }
 
diff --git a/web/src/app/modules/shared/services/loading/loading.service.ts b/web/src/app/modules/shared/services/loading/loading.service.ts
index 3c4e1ab0..8b9e5066 100644
--- a/web/src/app/modules/shared/services/loading/loading.service.ts
+++ b/web/src/app/modules/shared/services/loading/loading.service.ts
@@ -9,7 +9,7 @@ import {
   distinctUntilChanged,
   mapTo,
   startWith,
-  takeUntil,
+  takeWhile,
 } from 'rxjs/operators';
 
 @Injectable({
@@ -20,17 +20,12 @@ export class LoadingService {
 
   constructor() {}
 
-  withDelay(
-    watch: Observable<boolean>,
-    after: number,
-    atLeast: number
-  ): Observable<boolean> {
-    const loadingTimer = timer(after).pipe(takeUntil(watch));
-    const holdTimer = timer(after + atLeast);
-
-    return merge<boolean>(
-      loadingTimer.pipe(mapTo(true)),
-      combineLatest([watch, holdTimer]).pipe(mapTo(false))
-    ).pipe(startWith(false), distinctUntilChanged());
-  }
+  public showSpinner: Observable<boolean> = merge(
+    timer(650).pipe(
+      // show only if operation > 650ms
+      mapTo(true),
+      takeWhile(() => !this.requestComplete.value)
+    ), // if shown, stay at least 1sec to prevent flicker
+    combineLatest([this.requestComplete, timer(1650)]).pipe(mapTo(false))
+  ).pipe(startWith(false), distinctUntilChanged());
 }
diff --git a/web/src/app/modules/shared/services/websocket/websocket.service.ts b/web/src/app/modules/shared/services/websocket/websocket.service.ts
index c8c2f5a9..88bdb308 100644
--- a/web/src/app/modules/shared/services/websocket/websocket.service.ts
+++ b/web/src/app/modules/shared/services/websocket/websocket.service.ts
@@ -4,12 +4,7 @@
  */
 
 import { Injectable } from '@angular/core';
-import {
-  BehaviorSubject,
-  ObjectUnsubscribedError,
-  Observable,
-  Subject,
-} from 'rxjs';
+import { BehaviorSubject, Observable, Subject } from 'rxjs';
 import {
   NotifierService,
   NotifierSession,
@@ -188,9 +183,7 @@ export class WebsocketService implements BackendService {
         );
       }
     } catch (err) {
-      if (!(err instanceof ObjectUnsubscribedError)) {
-        console.error('parse websocket', err, data);
-      }
+      console.error('parse websocket', err, data);
     }
   }
 }
diff --git a/web/src/app/modules/shared/shared.module.ts b/web/src/app/modules/shared/shared.module.ts
index 9a22f0f5..4ccf7d75 100644
--- a/web/src/app/modules/shared/shared.module.ts
+++ b/web/src/app/modules/shared/shared.module.ts
@@ -43,6 +43,7 @@ import { ContentFilterComponent } from './components/presentation/content-filter
 import { HeptagonGridComponent } from './components/presentation/heptagon-grid/heptagon-grid.component';
 import { HeptagonGridRowComponent } from './components/presentation/heptagon-grid-row/heptagon-grid-row.component';
 import { HeptagonLabelComponent } from './components/presentation/heptagon-label/heptagon-label.component';
+import { ContentSwitcherComponent } from './components/presentation/content-switcher/content-switcher.component';
 import { ObjectStatusComponent } from './components/presentation/object-status/object-status.component';
 import { PodStatusComponent } from './components/presentation/pod-status/pod-status.component';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -71,10 +72,6 @@ import { FilterDeletedDatagridRowPipe } from './pipes/filterDeletedDatagridRow/f
 import { ContentTextFilterComponent } from './components/presentation/content-text-filter/content-text-filter.component';
 import { TruncatePipe } from './pipes/truncate/truncate.pipe';
 import { StepperComponent } from './components/presentation/stepper/stepper.component';
-import { ViewHostDirective } from './directives/view-host/view-host.directive';
-import { dynamicComponents } from './dynamic-components';
-import { ViewContainerComponent } from './components/view/view-container.component';
-import { MissingComponentComponent } from './components/missing-component/missing-component.component';
 
 @NgModule({
   declarations: [
@@ -87,6 +84,7 @@ import { MissingComponentComponent } from './components/missing-component/missin
     CodeComponent,
     ContainersComponent,
     ContentFilterComponent,
+    ContentSwitcherComponent,
     ContextSelectorComponent,
     CytoscapeComponent,
     Cytoscape2Component,
@@ -141,9 +139,6 @@ import { MissingComponentComponent } from './components/missing-component/missin
     HelperComponent,
     ContentTextFilterComponent,
     StepperComponent,
-    ViewHostDirective,
-    ViewContainerComponent,
-    MissingComponentComponent,
   ],
   imports: [
     ClarityModule,
@@ -159,7 +154,7 @@ import { MissingComponentComponent } from './components/missing-component/missin
     ResizableModule,
     RouterModule,
   ],
-  providers: [highlightProvider(), dynamicComponents()],
+  providers: [highlightProvider()],
   exports: [
     FormatPathPipe,
     AlertComponent,
@@ -171,6 +166,7 @@ import { MissingComponentComponent } from './components/missing-component/missin
     CodeComponent,
     ContainersComponent,
     ContentFilterComponent,
+    ContentSwitcherComponent,
     ContextSelectorComponent,
     CytoscapeComponent,
     Cytoscape2Component,
@@ -217,8 +213,6 @@ import { MissingComponentComponent } from './components/missing-component/missin
     PreferencesComponent,
     HelperComponent,
     StepperComponent,
-    ViewHostDirective,
-    ViewContainerComponent,
   ],
 })
 export class SharedModule {}
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 16b9b204..8efd1317 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
@@ -12,7 +12,7 @@
 
   :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.
@@ -37,6 +37,7 @@
         display: none;
       }
     }
+
   }
 
   .header-centered {
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 69df534c..1c7cb8a4 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,9 +1,4 @@
-<div
-  clrFocusOnViewInit
-  class="content-component"
-  (scroll)="onScroll($event)"
-  [class.loading-content]="!hasReceivedContent"
->
+<div clrFocusOnViewInit class="content-component" (scroll)="onScroll($event)" [class.loading-content]="showSpinner || !hasReceivedContent">
   <ng-container *ngIf="hasReceivedContent && !showSpinner; else loading">
     <ng-container
       *ngIf="hasTabs; then withTabs; else withoutTabs"
@@ -11,12 +6,13 @@
     <ng-template #withTabs>
       <app-object-tabs
         [buttonGroup]="buttonGroup"
+        [iconName]="iconName"
         [title]="title"
         [views]="views"
       ></app-object-tabs>
     </ng-template>
     <ng-template #withoutTabs>
-      <app-view-container [view]="singleView"></app-view-container>
+      <app-content-switcher [view]="singleView"></app-content-switcher>
     </ng-template>
   </ng-container>
 
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 0e5209df..7ab35c8a 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
@@ -50,9 +50,21 @@ export class ContentComponent implements OnInit, OnDestroy {
     private router: Router,
     private iconService: IconService,
     private contentService: ContentService,
-    private loadingService: LoadingService
+    public loadingService: LoadingService
   ) {}
 
+  updatePath(url: string) {
+    const tree = this.router.parseUrl(url);
+
+    const primary = tree.root.children.primary;
+    let segments = [];
+    if (primary) {
+      segments = primary.segments;
+    }
+
+    this.handlePathChange(segments, tree.queryParams, false);
+  }
+
   ngOnInit() {
     this.updatePath(this.router.routerState.snapshot.url);
 
@@ -62,33 +74,17 @@ export class ContentComponent implements OnInit, OnDestroy {
       }
     );
 
-    this.loadingService
-      .withDelay(this.loadingService.requestComplete, 650, 1000)
-      .subscribe(v => {
-        this.showSpinner = v;
-      });
+    this.loadingSubscription = this.loadingService.showSpinner.subscribe(
+      value => {
+        this.showSpinner = value;
+      }
+    );
   }
 
   ngOnDestroy() {
     this.resetView();
-    if (this.contentSubscription) {
-      this.contentSubscription.unsubscribe();
-    }
-    if (this.loadingSubscription) {
-      this.loadingSubscription.unsubscribe();
-    }
-  }
-
-  updatePath(url: string) {
-    const tree = this.router.parseUrl(url);
-
-    const primary = tree.root.children.primary;
-    let segments = [];
-    if (primary) {
-      segments = primary.segments;
-    }
-
-    this.handlePathChange(segments, tree.queryParams, false);
+    this.contentSubscription.unsubscribe();
+    this.loadingSubscription.unsubscribe();
   }
 
   private handlePathChange(
@@ -133,7 +129,6 @@ export class ContentComponent implements OnInit, OnDestroy {
       this.hasReceivedContent = false;
       return;
     }
-
     this.buttonGroup = contentResponse.content.buttonGroup;
 
     this.extView = contentResponse.content.extensionComponent;
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 25187b2a..3ebc23fa 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
@@ -3,26 +3,19 @@
     <clr-dropdown class="dropdown-top" [clrCloseMenuOnItemClick]="true">
       <button type="button" class="dropdown-button" clrDropdownTrigger>
         <clr-icon shape="namespace"></clr-icon>
-        {{ currentNamespace }}
+      {{ currentNamespace }}
         <clr-icon shape="caret down"></clr-icon>
       </button>
-      <clr-dropdown-menu
-        class="dropdown-top"
-        *clrIfOpen
-        [clrPosition]="'bottom-right'"
-      >
+      <clr-dropdown-menu class="dropdown-top" *clrIfOpen [clrPosition]="'bottom-right'">
         <label class="dropdown-header">Namespaces</label>
-        <ng-container
-          *ngFor="let namespace of namespaces; trackBy: trackByIdentity"
-        >
+        <ng-container *ngFor="let namespace of namespaces;trackBy: trackByIdentity">
           <button
             type="button"
             class="dropdown-button"
             [ngClass]="namespaceClass(namespace)"
             clrDropdownItem
-            (click)="selectNamespace(namespace)"
-          >
-            {{ namespace }}
+            (click)="selectNamespace(namespace)">
+          {{ namespace }}
           </button>
         </ng-container>
       </clr-dropdown-menu>
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 7310874e..7be272af 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
@@ -28,5 +28,5 @@
 }
 
 .dropdown .dropdown-toggle:not(.btn) {
-  padding: 0 1rem 0 0;
+  padding: 0 1.0rem 0 0;
 }
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 d71153c2..6df7d749 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,71 +1,44 @@
-<clr-vertical-nav
-  [clrVerticalNavCollapsible]="true"
-  [(clrVerticalNavCollapsed)]="collapsed"
-  (clrVerticalNavCollapsedChange)="updateNavCollapsed($event)"
->
+<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
-      "
-    >
+  <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>
 
-        <a
-          clrVerticalNavLink
-          (click)="openPopup(i)"
-          [ngClass]="{ 'item-selected': i == this.lastSelection }"
-          [routerLinkActiveOptions]="{ exact: !section.children?.length }"
-          [routerLink]="formatPath(section.path)"
-          routerLinkActive="active"
-          (mouseover)="flyoutIndex = i; lastSelection = i"
+        <a clrVerticalNavLink
+           (click)="openPopup(i)"
+           [ngClass]= "{'item-selected' : i == this.lastSelection}"
+           [routerLinkActiveOptions]="{exact: !section.children?.length}"
+           [routerLink]="formatPath(section.path)"
+           routerLinkActive="active"
+           (mouseover)="flyoutIndex= i; lastSelection=i"
         >
-          <clr-icon
-            [attr.shape]="section.iconName | default: 'home'"
-            clrVerticalNavIcon
-          ></clr-icon>
+          <clr-icon [attr.shape]="section.iconName | default:'home'" clrVerticalNavIcon></clr-icon>
         </a>
 
-        <div
-          class="flyout nav-group-children"
-          [ngClass]="{ 'flyout-empty': !section.children?.length }"
-          (mouseleave)="flyoutIndex = -1; lastSelection = -1"
-          (document:mouseleave)="flyoutIndex = -1; lastSelection = -1"
+        <div class="flyout nav-group-children"
+              [ngClass]= "{'flyout-empty' : !section.children?.length}"
+              (mouseleave)="flyoutIndex= -1; lastSelection= -1"
+              (document:mouseleave)="flyoutIndex= -1; lastSelection= -1"
         >
-          <a
-            class="flyout-header"
-            (click)="openPopup(i)"
-            [routerLinkActiveOptions]="{ exact: !section.children?.length }"
-            [routerLink]="formatPath(section.path)"
-            routerLinkActive="active"
+          <a class="flyout-header"
+              (click)="openPopup(i)"
+              [routerLinkActiveOptions]="{exact: !section.children?.length}"
+              [routerLink]="formatPath(section.path)"
+              routerLinkActive="active"
           >
             {{ section.title }}
           </a>
           <clr-vertical-nav-group-children class="flyout-group">
-            <ng-container
-              *ngFor="
-                let category of section.children;
-                trackBy: identifyNavigationItem
-              "
-            >
-              <a
-                [routerLink]="formatPath(category.path)"
-                [routerLinkActiveOptions]="{ exact: true }"
-                routerLinkActive="active"
-                class="flyout-item nav-link"
-                (click)="setNavState(false, i)"
+            <ng-container *ngFor="let category of section.children; trackBy: identifyNavigationItem">
+              <a [routerLink]="formatPath(category.path)"
+                  [routerLinkActiveOptions]="{exact: true}"
+                  routerLinkActive="active"
+                  class="flyout-item nav-link"
+                  (click)="setNavState(false, i)"
               >
                 {{ category.title }}
               </a>
@@ -79,45 +52,30 @@
           <div class="nav-divider"></div>
           <span class="title">{{ section.module }}</span>
         </ng-template>
-        <ng-container
-          *ngIf="section.children?.length > 0; else noChildExpanded"
-        >
+        <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>
+            <clr-icon [attr.shape]="section.iconName | default:'home'" clrVerticalNavIcon></clr-icon>
             {{ section.title }}
             <clr-vertical-nav-group-children>
-              <a
-                clrVerticalNavLink
-                [routerLink]="formatPath(section.path)"
-                [routerLinkActiveOptions]="{ exact: true }"
-                routerLinkActive="active"
+              <a clrVerticalNavLink
+                 [routerLink]="formatPath(section.path)"
+                 [routerLinkActiveOptions]="{exact: true}"
+                 routerLinkActive="active"
               >
                 Overview
               </a>
-              <ng-container
-                *ngFor="
-                  let category of section.children;
-                  trackBy: identifyNavigationItem
-                "
-              >
+              <ng-container *ngFor="let category of section.children; trackBy: identifyNavigationItem">
                 <a
                   clrVerticalNavLink
                   [routerLink]="formatPath(category.path)"
-                  [routerLinkActiveOptions]="{ exact: true }"
+                  [routerLinkActiveOptions]="{exact: true}"
                   routerLinkActive="active"
                 >
-                  <clr-icon
-                    *ngIf="itemIcon(category) as categoryIcon"
-                    [attr.shape]="categoryIcon"
-                    clrVerticalNavIcon
-                  ></clr-icon>
+                  <clr-icon *ngIf="itemIcon(category) as categoryIcon" [attr.shape]="categoryIcon" clrVerticalNavIcon></clr-icon>
                   {{ category.title }}
                 </a>
               </ng-container>
@@ -125,17 +83,13 @@
           </clr-vertical-nav-group>
         </ng-container>
         <ng-template #noChildExpanded>
-          <a
-            clrVerticalNavLink
-            (click)="closePopups(i)"
-            [routerLink]="formatPath(section.path)"
-            [routerLinkActiveOptions]="{ exact: true }"
-            routerLinkActive="active"
+          <a clrVerticalNavLink
+             (click)="closePopups(i)"
+             [routerLink]="formatPath(section.path)"
+             [routerLinkActiveOptions]="{exact: true}"
+             routerLinkActive="active"
           >
-            <clr-icon
-              [attr.shape]="section.iconName | default: 'home'"
-              clrVerticalNavIcon
-            ></clr-icon>
+            <clr-icon [attr.shape]="section.iconName | default:'home'" clrVerticalNavIcon></clr-icon>
             <div class="nav-item-text">
               {{ section.title }}
             </div>
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 233fa853..2ea47833 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
@@ -8,10 +8,7 @@
   --flyout-background-color: var(--clr-color-neutral-50);
   --flyout-color: var(--clr-global-font-color);
   --flyout-header-color: var(--clr-vertical-nav-item-color, #666666);
-  --flyout-header-active-color: var(
-    --clr-vertical-nav-item-active-color,
-    #666666
-  );
+  --flyout-header-active-color: var(--clr-vertical-nav-item-active-color, #666666);
 }
 
 :host-context(body.dark) {
@@ -159,4 +156,5 @@
   .flyout:hover {
     display: flex;
   }
+
 }
diff --git a/web/src/app/modules/sugarloaf/components/smart/notifier/notifier.component.html b/web/src/app/modules/sugarloaf/components/smart/notifier/notifier.component.html
index 63d983f7..2f910909 100644
--- a/web/src/app/modules/sugarloaf/components/smart/notifier/notifier.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/notifier/notifier.component.html
@@ -48,9 +48,9 @@
 
 <ng-container *ngIf="!loading && success">
   <clr-alert
-    [clrAlertType]="'success'"
-    [clrAlertIcon]="'check-circle'"
-    [clrAlertAppLevel]="true"
+          [clrAlertType]="'success'"
+          [clrAlertIcon]="'check-circle'"
+          [clrAlertAppLevel]="true"
   >
     <clr-alert-item>
       <span class="alert-text">
@@ -59,3 +59,4 @@
     </clr-alert-item>
   </clr-alert>
 </ng-container>
+
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 99fc938c..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,21 +1,13 @@
 <div class="switch-button-container">
   <ng-container *ngIf="collapsed; else expanded">
-    <button
-      id="switchButton"
-      class="btn btn-link btn-inverse btn-collapsed"
-      (click)="switchTheme()"
-    >
+    <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>
+<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/uploader/uploader.component.html b/web/src/app/modules/sugarloaf/components/smart/uploader/uploader.component.html
index b701691d..437c5e13 100644
--- a/web/src/app/modules/sugarloaf/components/smart/uploader/uploader.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/uploader/uploader.component.html
@@ -1,49 +1,25 @@
 <div *ngIf="showModal" class="modal">
-  <div class="modal-dialog modal-xl" role="dialog" aria-hidden="true">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h3 class="modal-title">Add a new kubeconfig</h3>
-      </div>
-      <div class="modal-body">
-        <form class="clr-form">
-          <label for="config" class="clr-control-label"
-            >Upload a kubeconfig</label
-          >
-          <div
-            class="clr-control-container"
-            [ngClass]="{ 'clr-error': inputValue === '' }"
-          >
-            <div class="clr-textarea-wrapper">
-              <textarea
-                id="config"
-                name="config"
-                [(ngModel)]="inputValue"
-                (ngModelChange)="updateInput($event)"
-                placeholder="Enter value here"
-                class="clr-textarea"
-              ></textarea>
-              <clr-icon
-                class="clr-validate-icon"
-                shape="exclamation-circle"
-              ></clr-icon>
+    <div class="modal-dialog modal-xl" role="dialog" aria-hidden="true">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h3 class="modal-title">Add a new kubeconfig</h3>
             </div>
-            <span *ngIf="inputValue === ''" class="clr-subtext"
-              >This can't be empty</span
-            >
-          </div>
-        </form>
-      </div>
-      <div class="modal-footer">
-        <button
-          (click)="upload()"
-          class="btn btn-primary"
-          type="button"
-          [disabled]="hasInput()"
-        >
-          Upload
-        </button>
-      </div>
+            <div class="modal-body">
+                <form class="clr-form">
+                    <label for="config" class="clr-control-label">Upload a kubeconfig</label>
+                    <div class="clr-control-container" [ngClass]="{'clr-error': inputValue === ''}">
+                        <div class="clr-textarea-wrapper">
+                            <textarea id="config" name="config" [(ngModel)]="inputValue" (ngModelChange)="updateInput($event)" placeholder="Enter value here" class="clr-textarea"></textarea>
+                            <clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
+                        </div>
+                        <span *ngIf="inputValue === ''" class="clr-subtext">This can't be empty</span>
+                    </div>
+                  </form>
+            </div>
+            <div class="modal-footer">
+                <button (click)="upload()" class="btn btn-primary" type="button" [disabled]="hasInput()">Upload</button>
+            </div>
+        </div>
     </div>
-  </div>
 </div>
-<div *ngIf="showModal" class="modal-backdrop" aria-hidden="true"></div>
+<div *ngIf="showModal" class="modal-backdrop" aria-hidden="true"></div>
\ No newline at end of file
diff --git a/web/src/app/modules/sugarloaf/components/smart/uploader/uploader.component.scss b/web/src/app/modules/sugarloaf/components/smart/uploader/uploader.component.scss
index 193ea7ab..392b0a3d 100644
--- a/web/src/app/modules/sugarloaf/components/smart/uploader/uploader.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/uploader/uploader.component.scss
@@ -1,13 +1,13 @@
 .clr-control-container {
-  width: 100%;
-  min-height: 400px;
+    width: 100%;
+    min-height: 400px;
 }
 
 .clr-textarea-wrapper {
-  min-height: inherit;
+    min-height: inherit;
 }
 
 .clr-textarea {
-  width: 100%;
-  min-height: inherit;
-}
+    width: 100%;
+    min-height: inherit;
+}
\ No newline at end of file
diff --git a/web/src/main.ts b/web/src/main.ts
index a38494c4..85c4ab04 100644
--- a/web/src/main.ts
+++ b/web/src/main.ts
@@ -2,6 +2,11 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
+// Note(marlon): zone.js seems to add a thread blocking event listener
+// to mouse scroll events: https://github.com/angular/zone.js/issues/1097
+// This makes all event listeners passive by default. To note when attaching
+// custom event listeners in our home.
+import 'default-passive-events';
 import { enableProdMode } from '@angular/core';
 import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
 import { AppModule } from './app/app.module';
diff --git a/web/src/stories/sandbox.stories.ts b/web/src/stories/sandbox.stories.ts
index 6ce3acb6..e4be277a 100644
--- a/web/src/stories/sandbox.stories.ts
+++ b/web/src/stories/sandbox.stories.ts
@@ -1,23 +1,23 @@
-import { storiesOf } from '@storybook/angular';
-import { object } from '@storybook/addon-knobs';
-import { big_data } from './overview.data';
+import {storiesOf} from '@storybook/angular';
+import {object} from "@storybook/addon-knobs";
+import {big_data} from "./overview.data";
 
 storiesOf('Other/Sandbox', module).add('Component Sandbox', () => {
   const view = object('JSON', big_data);
 
   return {
     props: {
-      view,
+      view: view,
     },
     template: `
       <div class="main-container">
           <div class="content-container">
               <div class="content-area">
-                <app-view-container [view]="view">
-                </app-view-container>
+                <app-content-switcher [view]="view">
+                </app-content-switcher>
               </div>
           </div>
       </div>
       `,
-  };
+  }
 });
diff --git a/web/tsconfig.compodoc.json b/web/tsconfig.compodoc.json
index 0f15325e..a3751745 100644
--- a/web/tsconfig.compodoc.json
+++ b/web/tsconfig.compodoc.json
@@ -156,6 +156,8 @@
     "src//app/modules/shared/components/presentation/quadrant/quadrant.component.spec.ts",
     "src//app/modules/shared/components/presentation/yaml/yaml.component.ts",
     "src//app/modules/shared/components/presentation/yaml/yaml.component.spec.ts",
+    "src//app/modules/shared/components/presentation/content-switcher/content-switcher.component.ts",
+    "src//app/modules/shared/components/presentation/content-switcher/content-switcher.component.spec.ts",
     "src//app/modules/shared/components/presentation/containers/containers.component.spec.ts",
     "src//app/modules/shared/components/presentation/containers/containers.component.ts",
     "src//app/modules/shared/components/presentation/text/text.component.spec.ts",
@@ -220,6 +222,6 @@
     "src//stories/graph.stories.ts",
     "src//stories/graph.data.ts",
     "src//stories/preferences.stories.ts",
-    "src//polyfills.ts"
-  ]
+    "src//polyfills.ts",
+  ],
 }


Test patch:
diff --git a/internal/api/container_logs_test.go b/internal/api/container_logs_test.go
index 0882a057..faf56bbd 100644
--- a/internal/api/container_logs_test.go
+++ b/internal/api/container_logs_test.go
@@ -38,7 +38,12 @@ func TestContainerLogs_SendLogEventsStops(t *testing.T) {
 
 	dashConfig := configFake.NewMockDash(controller)
 
-	eventType := event.NewLoggingEventType("test-ns", "test-pod")
+	key := store.Key{
+		Namespace: "test-ns",
+		Name:      "test-pod",
+	}
+
+	eventType := event.NewLoggingEventType(key.Namespace, key.Name)
 	logCh := make(chan container.LogEntry)
 
 	s := NewPodLogsStateManager(dashConfig)
@@ -64,7 +69,6 @@ func TestContainerLogs_SendLogEventsClientSend(t *testing.T) {
 
 	dashConfig := configFake.NewMockDash(controller)
 	client := newOctantClient()
-	defer client.Close()
 
 	key := store.Key{
 		Namespace: "test-ns",
@@ -103,15 +107,11 @@ func TestContainerLogs_SendLogEventsClientSend(t *testing.T) {
 type octantClient struct {
 	sendCalledWith event.Event
 	ch             chan bool
-	stopCh         chan struct{}
 }
 
-var _ OctantClient = &octantClient{}
-
 func newOctantClient() *octantClient {
 	return &octantClient{
-		ch:     make(chan bool, 1),
-		stopCh: make(chan struct{}, 1),
+		ch: make(chan bool, 1),
 	}
 }
 
@@ -120,12 +120,3 @@ func (oc *octantClient) Send(event event.Event) {
 	oc.ch <- true
 }
 func (oc *octantClient) ID() string { return "" }
-
-func (oc *octantClient) StopCh() <-chan struct{} {
-	return oc.stopCh
-}
-
-func (oc *octantClient) Close() {
-	close(oc.ch)
-	close(oc.stopCh)
-}
diff --git a/internal/api/content_manager_test.go b/internal/api/content_manager_test.go
index c1e5235e..6eb3354e 100644
--- a/internal/api/content_manager_test.go
+++ b/internal/api/content_manager_test.go
@@ -54,12 +54,9 @@ func TestContentManager_GenerateContent(t *testing.T) {
 	})
 	octantClient := fake.NewMockOctantClient(controller)
 
-	stopCh := make(chan struct{}, 1)
-
 	contentResponse := component.ContentResponse{}
 	contentEvent := api.CreateContentEvent(contentResponse, "default", "/path", params)
 	octantClient.EXPECT().Send(contentEvent).AnyTimes()
-	octantClient.EXPECT().StopCh().Return(stopCh).AnyTimes()
 
 	logger := log.NopLogger()
 
diff --git a/internal/describer/object_test.go b/internal/describer/object_test.go
index cdcde405..a5e4d1c5 100644
--- a/internal/describer/object_test.go
+++ b/internal/describer/object_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	"github.com/vmware-tanzu/octant/pkg/event/fake"
+
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/vmware-tanzu/octant/internal/describer"
@@ -45,8 +47,9 @@ func TestObjectDescriber(t *testing.T) {
 	dashConfig := configFake.NewMockDash(controller)
 	moduleRegistrar := pluginFake.NewMockModuleRegistrar(controller)
 	actionRegistrar := pluginFake.NewMockActionRegistrar(controller)
+	wsClient := fake.NewMockWSClientGetter(controller)
 
-	pluginManager := plugin.NewManager(nil, moduleRegistrar, actionRegistrar)
+	pluginManager := plugin.NewManager(nil, moduleRegistrar, actionRegistrar, wsClient)
 	dashConfig.EXPECT().PluginManager().Return(pluginManager).AnyTimes()
 
 	podSummary := component.NewText("summary")
diff --git a/pkg/plugin/javascript/dashboard_send_event_test.go b/pkg/plugin/javascript/dashboard_send_event_test.go
new file mode 100644
index 00000000..96c7c3ba
--- /dev/null
+++ b/pkg/plugin/javascript/dashboard_send_event_test.go
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package javascript
+
+import (
+	"context"
+	"testing"
+
+	"github.com/vmware-tanzu/octant/pkg/action"
+
+	"github.com/vmware-tanzu/octant/pkg/event"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/require"
+
+	"github.com/vmware-tanzu/octant/pkg/event/fake"
+)
+
+func TestDashboardSendEvent_Name(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+	wsClient := fake.NewMockWSClientGetter(ctrl)
+
+	d := NewDashboardSendEvent(wsClient)
+
+	want := "SendEvent"
+	got := d.Name()
+
+	require.Equal(t, want, got)
+}
+
+func TestDashboardSendEvent_Call(t *testing.T) {
+	type ctorArgs struct {
+		wsClient func(ctx context.Context, ctrl *gomock.Controller) event.WSClientGetter
+	}
+	tests := []struct {
+		name     string
+		ctorArgs ctorArgs
+		call     string
+		wantErr  bool
+	}{
+		{
+			name: "empty clientID",
+			ctorArgs: ctorArgs{
+				wsClient: func(ctx context.Context, ctrl *gomock.Controller) event.WSClientGetter {
+					wsClient := fake.NewMockWSClientGetter(ctrl)
+					return wsClient
+				},
+			},
+			call:    `dashClient.SendEvent("", "event.octant.dev/alert", {namespace:'test', apiVersion: 'v1', kind:'Pod'})`,
+			wantErr: true,
+		},
+		{
+			name: "action sends",
+			ctorArgs: ctorArgs{
+				wsClient: func(ctx context.Context, ctrl *gomock.Controller) event.WSClientGetter {
+					event := event.CreateEvent(event.EventTypeAlert, action.Payload{"namespace": "test", "apiVersion": "v1", "kind": "ReplicaSet"})
+
+					wsClient := fake.NewMockWSClientGetter(ctrl)
+					wsEventSender := fake.NewMockWSEventSender(ctrl)
+
+					wsClient.EXPECT().Get("my-client-id").Return(wsEventSender)
+					wsEventSender.EXPECT().Send(event)
+					return wsClient
+				},
+			},
+			call:    `dashClient.SendEvent("my-client-id", "event.octant.dev/alert", {namespace:'test', apiVersion: 'v1', kind:'ReplicaSet'})`,
+			wantErr: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			ctx := context.Background()
+			d := NewDashboardSendEvent(tt.ctorArgs.wsClient(ctx, ctrl))
+
+			runner := functionRunner{wantErr: tt.wantErr}
+			runner.run(ctx, t, d, tt.call)
+
+		})
+	}
+}
diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go
index 7da23348..09ee0e6e 100644
--- a/pkg/plugin/manager_test.go
+++ b/pkg/plugin/manager_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	fake2 "github.com/vmware-tanzu/octant/pkg/event/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/hashicorp/go-plugin"
 	"github.com/stretchr/testify/assert"
@@ -59,6 +61,7 @@ func TestManager(t *testing.T) {
 	clientFactory := fake.NewMockClientFactory(controller)
 	moduleRegistrar := fake.NewMockModuleRegistrar(controller)
 	actionRegistrar := fake.NewMockActionRegistrar(controller)
+	wsClient := fake2.NewMockWSClientGetter(controller)
 
 	name := "plugin1"
 
@@ -76,7 +79,7 @@ func TestManager(t *testing.T) {
 	})
 
 	apiService := &stubAPIService{}
-	manager := dashPlugin.NewManager(apiService, moduleRegistrar, actionRegistrar, options...)
+	manager := dashPlugin.NewManager(apiService, moduleRegistrar, actionRegistrar, wsClient, options...)
 
 	manager.SetStore(store)
 
@@ -101,6 +104,7 @@ func TestManager_Print(t *testing.T) {
 	store := fake.NewMockManagerStore(controller)
 	moduleRegistrar := fake.NewMockModuleRegistrar(controller)
 	actionRegistrar := fake.NewMockActionRegistrar(controller)
+	wsClient := fake2.NewMockWSClientGetter(controller)
 
 	store.EXPECT().ClientNames().Return([]string{"plugin1", "plugin2"})
 
@@ -131,7 +135,7 @@ func TestManager_Print(t *testing.T) {
 	})
 
 	apiService := &stubAPIService{}
-	manager := dashPlugin.NewManager(apiService, moduleRegistrar, actionRegistrar, options...)
+	manager := dashPlugin.NewManager(apiService, moduleRegistrar, actionRegistrar, wsClient, options...)
 	manager.SetStore(store)
 
 	ctx := context.Background()
@@ -158,6 +162,7 @@ func TestManager_Tabs(t *testing.T) {
 	store := fake.NewMockManagerStore(controller)
 	moduleRegistrar := fake.NewMockModuleRegistrar(controller)
 	actionRegistrar := fake.NewMockActionRegistrar(controller)
+	wsClient := fake2.NewMockWSClientGetter(controller)
 
 	store.EXPECT().ClientNames().Return([]string{"plugin1", "plugin2"})
 
@@ -179,7 +184,7 @@ func TestManager_Tabs(t *testing.T) {
 	})
 
 	apiService := &stubAPIService{}
-	manager := dashPlugin.NewManager(apiService, moduleRegistrar, actionRegistrar, options...)
+	manager := dashPlugin.NewManager(apiService, moduleRegistrar, actionRegistrar, wsClient, options...)
 	manager.SetStore(store)
 
 	ctx := context.Background()