Repository: vmware-tanzu/octant
Pull request #1424 (2020-09-28 23:52:50)

Unmarshal resource viewer node paths
**Special notes for your reviewer**:

Another plugin unmarshal bug 😞 

**Release note**:
```
n/a
```



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dadfa0eb..5fddfdd2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,9 @@
   * [Unreleased Changes][0]
 
 ## Current release:
-  * [CHANGELOG-0.16.md][16]
+  * [CHANGELOG-0.15.md][15]
 
 ## Older releases:
-  * [CHANGELOG-0.15.md][15]
   * [CHANGELOG-0.14.md][14]
   * [CHANGELOG-0.13.md][13]
   * [CHANGELOG-0.12.md][12]
@@ -31,4 +30,3 @@
 [13]: https://github.com/vmware-tanzu/octant/blob/master/changelogs/CHANGELOG-0.13.md
 [14]: https://github.com/vmware-tanzu/octant/blob/master/changelogs/CHANGELOG-0.14.md
 [15]: https://github.com/vmware-tanzu/octant/blob/master/changelogs/CHANGELOG-0.15.md
-[16]: https://github.com/vmware-tanzu/octant/blob/master/changelogs/CHANGELOG-0.16.md
diff --git a/README.md b/README.md
index b8a8be54..6f47c018 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 ![Logo][octant-logo]
 
-[![Build Status](https://github.com/vmware-tanzu/octant/workflows/preflight-checks/badge.svg)](https://github.com/vmware-tanzu/octant/workflows/preflight-checks/badge.svg)
+[![Build Status](https://cloud.drone.io/api/badges/vmware-tanzu/octant/status.svg)](https://cloud.drone.io/vmware-tanzu/octant)
 ![GitHub release](https://img.shields.io/github/release/vmware-tanzu/octant.svg)
 [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
 
diff --git a/build.go b/build.go
index b3741e7f..2383b0a3 100644
--- a/build.go
+++ b/build.go
@@ -25,7 +25,7 @@ import (
 )
 
 var (
-	VERSION    = "v0.16.0"
+	VERSION    = "v0.15.0"
 	GOPATH     = os.Getenv("GOPATH")
 	GIT_COMMIT = gitCommit()
 	BUILD_TIME = time.Now().UTC().Format(time.RFC3339)
diff --git a/changelogs/CHANGELOG-0.14.md b/changelogs/CHANGELOG-0.14
similarity index 100%
rename from changelogs/CHANGELOG-0.14.md
rename to changelogs/CHANGELOG-0.14
diff --git a/changelogs/CHANGELOG-0.15.md b/changelogs/CHANGELOG-0.15
similarity index 100%
rename from changelogs/CHANGELOG-0.15.md
rename to changelogs/CHANGELOG-0.15
diff --git a/changelogs/CHANGELOG-0.16.md b/changelogs/CHANGELOG-0.16.md
deleted file mode 100644
index 2428f9fe..00000000
--- a/changelogs/CHANGELOG-0.16.md
+++ /dev/null
@@ -1,34 +0,0 @@
-## v0.16.0
-#### 2020-09-24
-
-### Download
- - https://github.com/vmware-tanzu/octant/releases/v0.16.0
-
-### Highlights
-  * Added a 404-style error page when a resource is not found (#422, @scothis)
-  * Changed default log viewer to show last 5 minutes and allow selecting a broader range (#1209, @wwitzel3)
-  * Added `SendAlert` to plugin interface (#1216, @GuessWhoSamFoo)
-  * Changed to dynamic component loading (#1242, @bryanl)
-  * Added `Ctrl+/` keyboard shortcut and list of keyboard shortcuts (#1319, @wwitzel3)
-  * Updated quick switcher UI and added namespace to search (#1381, @GuessWhoSamFoo)
-
-### Bug Fixes
-  * Fixed default provided namespaces to initial namespace when empty (#838, @wwitzel3)
-  * Fixed problem with Storybook rendering of dynamic components (#1289, @mklanjsek)
-  * Fixed editing service to show sorted selectors (#1302, @GuessWhoSamFoo)
-  * Fixed safari height bug in the header so all browsers render the header the same. (#1313, @alexbarbato)
-  * Fixed configuring GRPC message size to API Client (#1324, @nodece)
-  * Fixed compiler warnings by colidating SCSS dependencies (#1357, @mklanjsek)
-  * Fixed pv list generation when claimRef pvc cannot be found (#1358, @GuessWhoSamFoo)
-  * Fixed unreferenced ConfigMap crashing summary tab (#1362, @GuessWhoSamFoo)
-
-### All Changes
-  * Exposed full selector capabilities through `Key` object (#1201, @ipsi)
-  * Updated build to use Golang 1.15 (#1248, @scothis)
-  * Added `SendEvent` support to JavaScript plugin runtime (#1290, @wwitzel3)
-  * Added an optional button group to data grid tables (#1299, @scothis)
-  * Added validator and action payload for stepper (#1300, @GuessWhoSamFoo)
-  * Added support for selectors in JavaScript plugin dashboard client (#1304, @bryanl)
-  * Added modal component and opening modals through buttons (#1305, @GuessWhoSamFoo)
-  * Added an Octant log sink for zap message (#1321, @bryanl)
-  * Changed to icons and colors for indicator to make it more accessible (#1335, @wwitzel3)
diff --git a/changelogs/unreleased/1201-ipsi b/changelogs/unreleased/1201-ipsi
new file mode 100644
index 00000000..8f963374
--- /dev/null
+++ b/changelogs/unreleased/1201-ipsi
@@ -0,0 +1,1 @@
+Expose full selector capabilities through `Key` object
diff --git a/changelogs/unreleased/1209-wwitzel3 b/changelogs/unreleased/1209-wwitzel3
new file mode 100644
index 00000000..d0191b74
--- /dev/null
+++ b/changelogs/unreleased/1209-wwitzel3
@@ -0,0 +1,2 @@
+Default log viewer last 5 minutes and allow selecting a broader range
+
diff --git a/changelogs/unreleased/1216-GuessWhoSamFoo b/changelogs/unreleased/1216-GuessWhoSamFoo
new file mode 100644
index 00000000..3ded470e
--- /dev/null
+++ b/changelogs/unreleased/1216-GuessWhoSamFoo
@@ -0,0 +1,1 @@
+Added `SendAlert` to plugin interface
diff --git a/changelogs/unreleased/1248-scothis b/changelogs/unreleased/1248-scothis
new file mode 100644
index 00000000..7b029835
--- /dev/null
+++ b/changelogs/unreleased/1248-scothis
@@ -0,0 +1,1 @@
+Use Golang 1.15
diff --git a/changelogs/unreleased/1289-mklanjsek b/changelogs/unreleased/1289-mklanjsek
new file mode 100644
index 00000000..9f55f347
--- /dev/null
+++ b/changelogs/unreleased/1289-mklanjsek
@@ -0,0 +1,1 @@
+Fixed problem with Storybook rendering of dynamic components
diff --git a/changelogs/unreleased/1299-scothis b/changelogs/unreleased/1299-scothis
new file mode 100644
index 00000000..7270639e
--- /dev/null
+++ b/changelogs/unreleased/1299-scothis
@@ -0,0 +1,1 @@
+Add an optional button group to data grid tables
diff --git a/changelogs/unreleased/1300-GuessWhoSamFoo b/changelogs/unreleased/1300-GuessWhoSamFoo
new file mode 100644
index 00000000..a51a67fa
--- /dev/null
+++ b/changelogs/unreleased/1300-GuessWhoSamFoo
@@ -0,0 +1,1 @@
+Added validator and action payload for stepper
diff --git a/changelogs/unreleased/1313-alexbarbato b/changelogs/unreleased/1313-alexbarbato
new file mode 100644
index 00000000..07b9f664
--- /dev/null
+++ b/changelogs/unreleased/1313-alexbarbato
@@ -0,0 +1,1 @@
+Fixing safari height bug in the header so all browsers render the header the same.
\ No newline at end of file
diff --git a/changelogs/unreleased/1319-wwitzel3 b/changelogs/unreleased/1319-wwitzel3
new file mode 100644
index 00000000..04ba47d1
--- /dev/null
+++ b/changelogs/unreleased/1319-wwitzel3
@@ -0,0 +1,1 @@
+add Ctrl+/ keyboard shortcut and list of keyboard shortcuts
diff --git a/changelogs/unreleased/1324-nodece b/changelogs/unreleased/1324-nodece
new file mode 100644
index 00000000..6aba6164
--- /dev/null
+++ b/changelogs/unreleased/1324-nodece
@@ -0,0 +1,1 @@
+Add configure GRPC message size to API Client
diff --git a/changelogs/unreleased/1335-wwitzel3 b/changelogs/unreleased/1335-wwitzel3
new file mode 100644
index 00000000..e151684e
--- /dev/null
+++ b/changelogs/unreleased/1335-wwitzel3
@@ -0,0 +1,1 @@
+use icons and colors for indicator to make it more accessible
diff --git a/changelogs/unreleased/1358-GuessWhoSamFoo b/changelogs/unreleased/1358-GuessWhoSamFoo
new file mode 100644
index 00000000..fee00cca
--- /dev/null
+++ b/changelogs/unreleased/1358-GuessWhoSamFoo
@@ -0,0 +1,1 @@
+Fixed pv list generation when claimRef pvc cannot be found
diff --git a/changelogs/unreleased/1381-GuessWhoSamFoo b/changelogs/unreleased/1381-GuessWhoSamFoo
new file mode 100644
index 00000000..e879eaa6
--- /dev/null
+++ b/changelogs/unreleased/1381-GuessWhoSamFoo
@@ -0,0 +1,1 @@
+Updated quick switcher UI and added namespace to search
diff --git a/changelogs/unreleased/422-scothis b/changelogs/unreleased/422-scothis
new file mode 100644
index 00000000..f48d6565
--- /dev/null
+++ b/changelogs/unreleased/422-scothis
@@ -0,0 +1,1 @@
+Render a 404-style error page when a resource is not found
diff --git a/changelogs/unreleased/838-wwitzel3 b/changelogs/unreleased/838-wwitzel3
new file mode 100644
index 00000000..802f2431
--- /dev/null
+++ b/changelogs/unreleased/838-wwitzel3
@@ -0,0 +1,1 @@
+default provided namespaces to initial namespace when empty
diff --git a/choco/octant.nuspec b/choco/octant.nuspec
index a37fca31..4479d9e8 100644
--- a/choco/octant.nuspec
+++ b/choco/octant.nuspec
@@ -3,7 +3,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
   <metadata>
     <id>octant</id>
-    <version>0.16.0</version>
+    <version>0.15.0</version>
     <owners>VMware</owners>
     <title>Octant</title>
     <authors>VMware Tanzu</authors>
diff --git a/choco/tools/chocolateyinstall.ps1 b/choco/tools/chocolateyinstall.ps1
index aaff53b0..d5ea3370 100644
--- a/choco/tools/chocolateyinstall.ps1
+++ b/choco/tools/chocolateyinstall.ps1
@@ -2,8 +2,8 @@
 $ErrorActionPreference = 'Stop';
 $packageName = 'octant'
 $toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
-$url64 = 'https://github.com/vmware-tanzu/octant/releases/download/v0.16.0/octant_0.16.0_Windows-64bit.zip'
-$checksum64 = '24dcb8e7cdf5643b4eb472510cfb8d113acab04a062f069c72d74bf0c4961091'
+$url64 = 'https://github.com/vmware-tanzu/octant/releases/download/v0.15.0/octant_0.15.0_Windows-64bit.zip'
+$checksum64 = '963f50c196a56390127b01eabb49abaf0604f49a8c879ce4f28562d8d825b84d'
 $checksumType64= 'sha256'
 
 $packageArgs = @{
diff --git a/internal/api/container_logs.go b/internal/api/container_logs.go
index d03518ab..9080e9f0 100644
--- a/internal/api/container_logs.go
+++ b/internal/api/container_logs.go
@@ -32,7 +32,6 @@ type logEntry struct {
 const (
 	RequestPodLogsSubscribe   = "action.octant.dev/podLogs/subscribe"
 	RequestPodLogsUnsubscribe = "action.octant.dev/podLogs/unsubscribe"
-	DefaultSinceSeconds       = 300
 )
 
 type podLogsStateManager struct {
@@ -72,7 +71,6 @@ func (s *podLogsStateManager) StreamPodLogsSubscribe(_ octant.State, payload act
 	if err != nil {
 		return fmt.Errorf("getting namespace from payload: %w", err)
 	}
-
 	podName, err := payload.String("podName")
 	if err != nil {
 		return fmt.Errorf("getting podName from payload: %w", err)
@@ -83,25 +81,23 @@ func (s *podLogsStateManager) StreamPodLogsSubscribe(_ octant.State, payload act
 		return fmt.Errorf("getting containerName from payload: %w", err)
 	}
 
-	since, err := payload.Int64("sinceSeconds")
+	since, err := payload.Float64("sinceSeconds")
 	if err != nil {
 		return fmt.Errorf("getting since from payload: %w", err)
 	}
 
 	// Default to 5 minutes
-	sinceSeconds := int64(DefaultSinceSeconds)
-	// Allow negative since, negative since means since creation.
+	sinceDuration := time.Second * 300
 	if since != 0 {
-		sinceSeconds = since
+		sinceDuration = time.Second * time.Duration(since)
 	}
 
 	eventType := event.NewLoggingEventType(namespace, podName)
-
 	val, ok := s.podLogSubscriptions.Load(eventType)
 	if ok {
 		cancelFn, ok := val.(context.CancelFunc)
 		if !ok {
-			return fmt.Errorf("bad cancelFn conversion for: %s", eventType)
+			return fmt.Errorf("bad cancelFn conversion for %s", eventType)
 		}
 		cancelFn()
 	}
@@ -110,7 +106,7 @@ func (s *podLogsStateManager) StreamPodLogsSubscribe(_ octant.State, payload act
 	key.Name = podName
 	key.Namespace = namespace
 
-	logStreamer, err := container.NewLogStreamer(s.ctx, s.config, key, sinceSeconds, containerName)
+	logStreamer, err := container.NewLogStreamer(s.ctx, s.config, key, sinceDuration, containerName)
 	if err != nil {
 		return fmt.Errorf("creating log streamer: %w", err)
 	}
diff --git a/internal/api/navigation_manager.go b/internal/api/navigation_manager.go
index bc315933..d4a068d9 100644
--- a/internal/api/navigation_manager.go
+++ b/internal/api/navigation_manager.go
@@ -165,7 +165,7 @@ func NavigationGenerator(ctx context.Context, state octant.State, config Navigat
 	return sections, nil
 }
 
-// CreateNavigationEvent creates a navigation event.
+// CreateNavigationEvent creates a namespaces event.
 func CreateNavigationEvent(sections []navigation.Navigation, defaultPath string) oevent.Event {
 	return oevent.Event{
 		Type: oevent.EventTypeNavigation,
diff --git a/internal/modules/overview/container/logstreamer.go b/internal/modules/overview/container/logstreamer.go
index 1191ab1a..beff545e 100644
--- a/internal/modules/overview/container/logstreamer.go
+++ b/internal/modules/overview/container/logstreamer.go
@@ -11,9 +11,9 @@ import (
 	"fmt"
 	"io"
 	"sync"
+	"time"
 
 	corev1 "k8s.io/api/core/v1"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/vmware-tanzu/octant/internal/config"
 	"github.com/vmware-tanzu/octant/internal/util/kubernetes"
@@ -25,7 +25,6 @@ type logStreamer struct {
 	pod          string
 	containers   []string
 	sinceSeconds *int64
-	creationTime *v1.Time
 	stream       chan LogEntry
 
 	ctx      context.Context
@@ -37,7 +36,7 @@ type logStreamer struct {
 var _ LogStreamer = (*logStreamer)(nil)
 
 // NewLogStreamer returns an instance of a logStream configured to stream logs for the given namespace/pod/container(s).
-func NewLogStreamer(ctx context.Context, dashConfig config.Dash, key store.Key, sinceSeconds int64, containerNames ...string) (*logStreamer, error) {
+func NewLogStreamer(ctx context.Context, dashConfig config.Dash, key store.Key, sinceDuration time.Duration, containerNames ...string) (*logStreamer, error) {
 	ctx, cancelFn := context.WithCancel(ctx)
 
 	if shouldFetchContainerNames(containerNames) {
@@ -63,31 +62,13 @@ func NewLogStreamer(ctx context.Context, dashConfig config.Dash, key store.Key,
 		}
 	}
 
-	var creationTime *v1.Time
-	if sinceSeconds < 0 {
-		object, err := dashConfig.ObjectStore().Get(ctx, key)
-		if err != nil {
-			cancelFn()
-			return nil, fmt.Errorf("getting pod from objectstore: %w", err)
-		}
-
-		var pod corev1.Pod
-		err = kubernetes.FromUnstructured(object, &pod)
-
-		if err != nil {
-			cancelFn()
-			return nil, fmt.Errorf("converting unstructured: %w", err)
-		}
-
-		creationTime = &v1.Time{Time: pod.CreationTimestamp.Time}
-	}
+	sinceSeconds := int64(sinceDuration.Seconds())
 
 	return &logStreamer{
 		namespace:    key.Namespace,
 		pod:          key.Name,
 		containers:   containerNames,
 		sinceSeconds: &sinceSeconds,
-		creationTime: creationTime,
 		config:       dashConfig,
 		ctx:          ctx,
 		cancelFn:     cancelFn,
@@ -162,18 +143,11 @@ func (s *logStreamer) containerStream(container string) (io.ReadCloser, error) {
 	if err != nil {
 		return nil, err
 	}
-
-	options := &corev1.PodLogOptions{
-		Container:  container,
-		Follow:     true,
-		Timestamps: true,
-	}
-	if s.creationTime != nil {
-		options.SinceTime = s.creationTime
-	} else {
-		options.SinceSeconds = s.sinceSeconds
-	}
-
-	request := client.CoreV1().Pods(s.namespace).GetLogs(s.pod, options)
+	request := client.CoreV1().Pods(s.namespace).GetLogs(s.pod, &corev1.PodLogOptions{
+		Container:    container,
+		Follow:       true,
+		Timestamps:   true,
+		SinceSeconds: s.sinceSeconds,
+	})
 	return request.Stream(s.ctx)
 }
diff --git a/pkg/action/payload.go b/pkg/action/payload.go
index cfb23fca..ba138da2 100644
--- a/pkg/action/payload.go
+++ b/pkg/action/payload.go
@@ -74,24 +74,6 @@ func (p Payload) Uint16(key string) (uint16, error) {
 	return uint16(i), nil
 }
 
-// Int64 returns a int64 from the payload.
-func (p Payload) Int64(key string) (int64, error) {
-	i, found, err := unstructured.NestedFloat64(p, key)
-	if err != nil {
-		return 0, err
-	}
-
-	if !found {
-		return 0, errors.Errorf("payload does not contain %q", key)
-	}
-
-	if i > math.MaxInt64 || i < math.MinInt64 {
-		return 0, errors.Errorf("value %v is not a valid int64", i)
-	}
-
-	return int64(i), nil
-}
-
 // String returns a string from the payload.
 func (p Payload) String(key string) (string, error) {
 	s, ok := p[key].(string)
diff --git a/pkg/view/component/base.go b/pkg/view/component/base.go
index 772e7290..10f46d77 100644
--- a/pkg/view/component/base.go
+++ b/pkg/view/component/base.go
@@ -48,8 +48,6 @@ const (
 	TypeLoading = "loading"
 	// TypeLogs is a logs component.
 	TypeLogs = "logs"
-	// TypeModal is a modal component.
-	TypeModal = "modal"
 	// TypePodStatus is a pod status component.
 	TypePodStatus = "podStatus"
 	// TypePort is a port component.
diff --git a/pkg/view/component/button.go b/pkg/view/component/button.go
index 06c2bcd2..57a411ad 100644
--- a/pkg/view/component/button.go
+++ b/pkg/view/component/button.go
@@ -3,8 +3,6 @@ package component
 import (
 	"encoding/json"
 
-	"github.com/pkg/errors"
-
 	"github.com/vmware-tanzu/octant/pkg/action"
 )
 
@@ -27,51 +25,11 @@ func WithButtonConfirmation(title, body string) ButtonOption {
 	}
 }
 
-// WithModal configures a button to open a modal
-func WithModal(modal *Modal) ButtonOption {
-	return func(button *Button) {
-		button.Modal = modal
-	}
-}
-
-func (b *Button) UnmarshalJSON(data []byte) error {
-	x := struct {
-		Name         string         `json:"name"`
-		Payload      action.Payload `json:"payload"`
-		Confirmation *Confirmation  `json:"confirmation,omitempty"`
-		Modal        *TypedObject   `json:"modal,omitempty"`
-	}{}
-
-	if err := json.Unmarshal(data, &x); err != nil {
-		return err
-	}
-
-	if x.Modal != nil {
-		component, err := x.Modal.ToComponent()
-		if err != nil {
-			return err
-		}
-
-		modal, ok := component.(*Modal)
-		if !ok {
-			return errors.New("item was not a modal")
-		}
-		b.Modal = modal
-	}
-
-	b.Name = x.Name
-	b.Payload = x.Payload
-	b.Confirmation = x.Confirmation
-
-	return nil
-}
-
 // Button is a button in a group.
 type Button struct {
 	Name         string         `json:"name"`
 	Payload      action.Payload `json:"payload"`
 	Confirmation *Confirmation  `json:"confirmation,omitempty"`
-	Modal        Component      `json:"modal,omitempty"`
 }
 
 // NewButton creates an instance of Button.
diff --git a/pkg/view/component/form.go b/pkg/view/component/form.go
index 2d50cb45..e7d6e004 100644
--- a/pkg/view/component/form.go
+++ b/pkg/view/component/form.go
@@ -614,7 +614,6 @@ func (ff *FormFieldHidden) UnmarshalJSON(data []byte) error {
 
 type Form struct {
 	Fields []FormField `json:"fields"`
-	Action string      `json:"action,omitempty"`
 }
 
 func (f *Form) MarshalJSON() ([]byte, error) {
@@ -652,7 +651,6 @@ func (f *Form) UnmarshalJSON(data []byte) error {
 			Error         string                 `json:"error"`
 			Validators    []string               `json:"validators"`
 		} `json:"fields"`
-		Action string `json:"action,omitempty"`
 	}{}
 
 	err := json.Unmarshal(data, &x)
@@ -696,7 +694,6 @@ func (f *Form) UnmarshalJSON(data []byte) error {
 
 		f.Fields = append(f.Fields, ff)
 	}
-	f.Action = x.Action
 
 	return nil
 }
diff --git a/pkg/view/component/logs.go b/pkg/view/component/logs.go
index 0913f695..cce7a1b0 100644
--- a/pkg/view/component/logs.go
+++ b/pkg/view/component/logs.go
@@ -16,7 +16,7 @@ var defaultDurations = []Since{
 	{"1 hour", 3600},
 	{"3 hours", 10800},
 	{"5 hours", 18000},
-	{"Creation", -1},
+	{"Creation", 1<<63 - 1},
 }
 
 type Since struct {
diff --git a/pkg/view/component/modal.go b/pkg/view/component/modal.go
deleted file mode 100644
index 36aa948b..00000000
--- a/pkg/view/component/modal.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package component
-
-import (
-	"encoding/json"
-)
-
-type ModalSize string
-
-const (
-	// ModalSizeSmall is the smallest modal
-	ModalSizeSmall ModalSize = "sm"
-	// ModalSizeLarge is a large modal
-	ModalSizeLarge ModalSize = "lg"
-	// ModalSizeExtraLarge is the largest modal
-	ModalSizeExtraLarge ModalSize = "xl"
-)
-
-// ModalConfig is a configuration for the modal component.
-type ModalConfig struct {
-	Body      Component `json:"body,omitempty"`
-	Form      *Form     `json:"form,omitempty"`
-	Opened    bool      `json:"opened"`
-	ModalSize ModalSize `json:"size,omitempty"`
-	Buttons   []Button  `json:"buttons,omitempty"`
-}
-
-// UnmarshalJSON unmarshals a modal config from JSON.
-func (m *ModalConfig) UnmarshalJSON(data []byte) error {
-	x := struct {
-		Body      *TypedObject `json:"body,omitempty"`
-		Form      *Form        `json:"form,omitempty"`
-		Opened    bool         `json:"opened"`
-		ModalSize ModalSize    `json:"size,omitempty"`
-		Buttons   []Button     `json:"buttons,omitempty"`
-	}{}
-
-	if err := json.Unmarshal(data, &x); err != nil {
-		return err
-	}
-
-	if x.Body != nil {
-		var err error
-		m.Body, err = x.Body.ToComponent()
-		if err != nil {
-			return err
-		}
-	}
-
-	m.Form = x.Form
-	m.Opened = x.Opened
-	m.ModalSize = x.ModalSize
-	m.Buttons = x.Buttons
-	return nil
-}
-
-// Modal is a modal component.
-//
-// +octant:component
-type Modal struct {
-	Base
-	Config ModalConfig `json:"config"`
-}
-
-// NewModal creates a new modal.
-func NewModal(title []TitleComponent) *Modal {
-	return &Modal{
-		Base: newBase(TypeModal, title),
-	}
-}
-
-var _ Component = (*Modal)(nil)
-
-// SetBody sets the body of a modal.
-func (m *Modal) SetBody(body Component) {
-	m.Config.Body = body
-}
-
-// AddForm adds a form to a modal. It is added after the body.
-func (m *Modal) AddForm(form Form) {
-	m.Config.Form = &form
-}
-
-// SetSize sets the size of a modal. Size is medium by default.
-func (m *Modal) SetSize(size ModalSize) {
-	m.Config.ModalSize = size
-}
-
-// AddButton is a helper to add a custom button
-func (m *Modal) AddButton(button Button) {
-	m.Config.Buttons = append(m.Config.Buttons, button)
-}
-
-// Open opens a modal. A modal is closed by default.
-func (m *Modal) Open() {
-	m.Config.Opened = true
-}
-
-// Close closes a modal.
-func (m *Modal) Close() {
-	m.Config.Opened = false
-}
-
-type modalMarshal Modal
-
-// MarshalJSON marshal a modal to JSON.
-func (m *Modal) MarshalJSON() ([]byte, error) {
-	k := modalMarshal(*m)
-	k.Metadata.Type = TypeModal
-	return json.Marshal(&k)
-}
diff --git a/pkg/view/component/resource_viewer.go b/pkg/view/component/resource_viewer.go
index 35c78859..80185c07 100644
--- a/pkg/view/component/resource_viewer.go
+++ b/pkg/view/component/resource_viewer.go
@@ -7,6 +7,7 @@ package component
 
 import (
 	"encoding/json"
+	"fmt"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -74,7 +75,7 @@ func (n *Node) UnmarshalJSON(data []byte) error {
 		Kind       string         `json:"kind,omitempty"`
 		Status     NodeStatus     `json:"status,omitempty"`
 		Details    []*TypedObject `json:"details,omitempty"`
-		Path       *Link          `json:"path,omitempty"`
+		Path       *TypedObject   `json:"path,omitempty"`
 	}{}
 
 	if err := json.Unmarshal(data, &x); err != nil {
@@ -85,7 +86,6 @@ func (n *Node) UnmarshalJSON(data []byte) error {
 	n.APIVersion = x.APIVersion
 	n.Kind = x.Kind
 	n.Status = x.Status
-	n.Path = x.Path
 
 	if x.Details != nil {
 		n.Details = make([]Component, len(x.Details))
@@ -98,6 +98,18 @@ func (n *Node) UnmarshalJSON(data []byte) error {
 		n.Details[i] = dc
 	}
 
+	if x.Path != nil {
+		p, err := x.Path.ToComponent()
+		if err != nil {
+			return errors.Wrap(err, "unmarshal-ing detail")
+		}
+		l, ok := p.(*Link)
+		if !ok {
+			return fmt.Errorf("path must be a link, found %q", x.Path.Metadata.Type)
+		}
+		n.Path = l
+	}
+
 	return nil
 }
 
diff --git a/pkg/view/component/unmarshal.go b/pkg/view/component/unmarshal.go
index a9721a64..5ea7f17d 100644
--- a/pkg/view/component/unmarshal.go
+++ b/pkg/view/component/unmarshal.go
@@ -116,11 +116,6 @@ func unmarshal(to TypedObject) (Component, error) {
 		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
 			"unmarshal logs config")
 		o = t
-	case TypeModal:
-		t := &Modal{Base: Base{Metadata: to.Metadata}}
-		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
-			"unmarshal modal config")
-		o = t
 	case TypeQuadrant:
 		t := &Quadrant{Base: Base{Metadata: to.Metadata}}
 		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
diff --git a/site/docs/master/release.md b/site/docs/master/release.md
index e0d46fb6..429a5298 100644
--- a/site/docs/master/release.md
+++ b/site/docs/master/release.md
@@ -1,6 +1,6 @@
 ## Release Process
 
-Github Actions is used along with goreleaser for releases. This document is used to track the manual processes that occur during a release process.
+Drone Cloud is used along with goreleaser for releases. This document is used to track the manual processes that occur during a release process.
 
 ### Update version
 
diff --git a/web/package-lock.json b/web/package-lock.json
index b1b61ed2..e1689f15 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -10620,9 +10620,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "12.12.62",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.62.tgz",
-      "integrity": "sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg==",
+      "version": "12.12.54",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.54.tgz",
+      "integrity": "sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w==",
       "dev": true
     },
     "@types/node-fetch": {
@@ -15965,9 +15965,9 @@
       }
     },
     "electron": {
-      "version": "8.5.2",
-      "resolved": "https://registry.npmjs.org/electron/-/electron-8.5.2.tgz",
-      "integrity": "sha512-VU+zZnmCzxoZ5UfBg2UGVm+nyxlNlQOQkotMLfk7FCtnkIOhX+sosl618OCxUWjOvPc+Mpg5MEkEmxPU5ziW4Q==",
+      "version": "8.5.1",
+      "resolved": "https://registry.npmjs.org/electron/-/electron-8.5.1.tgz",
+      "integrity": "sha512-bsjMmjERUKFFh9JRX77rB/m3PizgxJvtAYnUkLFOj4nYofpRhaABkOSCzH/e/tPwHyu/PYh0oDfSXW6IEwG4gQ==",
       "dev": true,
       "requires": {
         "@electron/get": "^1.0.1",
@@ -26034,13 +26034,13 @@
       }
     },
     "roarr": {
-      "version": "2.15.4",
-      "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
-      "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
+      "version": "2.15.3",
+      "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.3.tgz",
+      "integrity": "sha512-AEjYvmAhlyxOeB9OqPUzQCo3kuAkNfuDk/HqWbZdFsqDFpapkTjiw+p4svNEoRLvuqNTxqfL+s+gtD4eDgZ+CA==",
       "dev": true,
       "optional": true,
       "requires": {
-        "boolean": "^3.0.1",
+        "boolean": "^3.0.0",
         "detect-node": "^2.0.4",
         "globalthis": "^1.0.1",
         "json-stringify-safe": "^5.0.1",
@@ -26102,9 +26102,9 @@
       "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
     },
     "rxjs": {
-      "version": "6.6.3",
-      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
-      "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==",
+      "version": "6.6.2",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
+      "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==",
       "requires": {
         "tslib": "^1.9.0"
       },
@@ -30695,9 +30695,9 @@
       "dev": true
     },
     "xterm": {
-      "version": "4.9.0",
-      "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.9.0.tgz",
-      "integrity": "sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw=="
+      "version": "4.8.1",
+      "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.8.1.tgz",
+      "integrity": "sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ=="
     },
     "xterm-addon-fit": {
       "version": "0.3.0",
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 1444aecf..bc33678c 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
@@ -2,13 +2,12 @@
   <clr-button
     *ngFor="let button of v.config.buttons; trackBy: trackByFn"
     class="{{ class }}"
-    (click)="onClick(button.payload, button.confirmation, button.modal)"
+    (click)="onClick(button.payload, button.confirmation)"
   >
     {{ button.name }}
   </clr-button>
 </clr-button-group>
 
-<app-view-modal *ngIf="modalView" [view]="modalView"></app-view-modal>
 <clr-modal [(clrModalOpen)]="isModalOpen">
   <h3 class="modal-title">{{ modalTitle }}</h3>
   <div class="modal-body">
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 f68b9d9e..da63978e 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,13 +1,7 @@
 import { Component, EventEmitter, Output } from '@angular/core';
-import {
-  ButtonGroupView,
-  Confirmation,
-  View,
-  ModalView,
-} from '../../../models/content';
+import { ButtonGroupView, Confirmation } from '../../../models/content';
 import { ActionService } from '../../../services/action/action.service';
 import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
-import { ModalService } from '../../../services/modal/modal.service';
 
 @Component({
   selector: 'app-button-group',
@@ -25,12 +19,7 @@ export class ButtonGroupComponent extends AbstractViewComponent<
   payload = {};
   class = '';
 
-  modalView: View;
-
-  constructor(
-    private actionService: ActionService,
-    private modalService: ModalService
-  ) {
+  constructor(private actionService: ActionService) {
     super();
   }
 
@@ -42,19 +31,11 @@ export class ButtonGroupComponent extends AbstractViewComponent<
         } else {
           this.class = 'btn-outline btn-sm';
         }
-        if (button.modal) {
-          this.modalView = button.modal;
-          const modal = this.modalView as ModalView;
-          this.modalService.setState(modal.config.opened);
-        }
       });
     }
   }
 
-  onClick(payload: {}, confirmation?: Confirmation, modal?: View) {
-    if (modal) {
-      this.modalService.openModal();
-    }
+  onClick(payload: {}, confirmation?: Confirmation) {
     if (confirmation) {
       this.activateModal(payload, confirmation);
     } else {
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 74923d87..07a1406e 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
@@ -1,56 +1,45 @@
-<div class="card">
-  <div class="card-block">
-    <ng-container
-      *ngTemplateOutlet="currentAction ? action : content"
-    ></ng-container>
-  </div>
-  <ng-container *ngTemplateOutlet="currentAction ? formFooter : actionFooter"></ng-container>
-</div>
-
+<ng-container
+  *ngTemplateOutlet="currentAction ? action : content"
+></ng-container>
 
 <ng-template #action>
-  <h3 class="card-title">{{ currentAction.title }}</h3>
   <app-form
     #appForm
     [form]="currentAction.form"
+    [title]="currentAction.title"
+    (submit)="onActionSubmit($event)"
+    (cancel)="onActionCancel()"
   >
   </app-form>
 </ng-template>
 
-<ng-template #formFooter>
-  <div class="card-footer">
-    <button class="btn btn-primary btn-sm" type="submit" (click)="onActionSubmit()">Submit</button>
-    <button class="btn btn-sm" type="button" (click)="onActionCancel()">
-      Cancel
-    </button>
-  </div>
-</ng-template>
-
-<ng-template #actionFooter>
-  <div class="card-footer" *ngIf="v.config.actions?.length > 0">
-    <ng-container *ngFor="let action of v.config.actions; trackBy: trackByFn">
-      <button class="btn btn-sm btn-link" (click)="setAction(action)">
-        {{ action.name }}
-      </button>
-    </ng-container>
-  </div>
-</ng-template>
-
 <ng-template #content>
-  <ng-container *ngIf="v.config.alert">
-    <div class="alert alert-{{ v.config.alert.type }} alert-sm">
-      <div class="alert-item static">
-        <span class="alert-text">
-          {{ v.config.alert.message }}
-        </span>
+  <div class="card">
+    <div class="card-block">
+      <ng-container *ngIf="v.config.alert">
+        <div class="alert alert-{{ v.config.alert.type }} alert-sm">
+          <div class="alert-item static">
+            <span class="alert-text">
+              {{ v.config.alert.message }}
+            </span>
+          </div>
+        </div>
+      </ng-container>
+      <h4 class="card-title">
+        <app-view-title [views]="title"></app-view-title>
+      </h4>
+
+      <div class="card-text">
+        <app-view-container [view]="body"></app-view-container>
       </div>
     </div>
-  </ng-container>
-  <h4 class="card-title">
-    <app-view-title [views]="title"></app-view-title>
-  </h4>
 
-  <div class="card-text">
-    <app-view-container [view]="body"></app-view-container>
+    <div class="card-footer" *ngIf="v.config.actions?.length > 0">
+      <ng-container *ngFor="let action of v.config.actions; trackBy: trackByFn">
+        <button class="btn btn-sm btn-link" (click)="setAction(action)">
+          {{ action.name }}
+        </button>
+      </ng-container>
+    </div>
   </div>
 </ng-template>
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 87dde1ee..16021f17 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
@@ -12,13 +12,10 @@ import { FormBuilder, FormGroup } from '@angular/forms';
 import { ViewService } from '../../../services/view/view.service';
 import { viewServiceStub } from 'src/app/testing/view-service.stub';
 import { SharedModule } from '../../../shared.module';
-import { FormComponent } from '../form/form.component';
 
 describe('CardComponent', () => {
   let component: CardComponent;
   let fixture: ComponentFixture<CardComponent>;
-  let formComponent: FormComponent;
-  let formFixture: ComponentFixture<FormComponent>;
   const formBuilder: FormBuilder = new FormBuilder();
 
   const action: Action = {
@@ -86,17 +83,12 @@ describe('CardComponent', () => {
       formGroupExample: 'justForTest',
     });
 
-    formFixture = TestBed.createComponent(FormComponent);
-    formComponent = formFixture.componentInstance;
-    formComponent.formGroup = formGroup;
-    component.appForm = formComponent;
-
-    component.onActionSubmit();
+    component.onActionSubmit(formGroup);
     expect(component.currentAction).toBeUndefined();
   });
 
   it('should not submit action', () => {
-    component.onActionSubmit();
+    component.onActionSubmit({} as FormGroup);
     expect(component.currentAction).toBeDefined();
   });
 
@@ -125,7 +117,10 @@ describe('CardComponent', () => {
 
   it('should call "onActionCancel" method when cancelling the form', fakeAsync(() => {
     spyOn(component, 'onActionCancel');
-    component.onActionCancel();
+    fixture.detectChanges();
+    component.appForm.onFormCancel();
+    tick();
+    fixture.detectChanges();
     expect(component.onActionCancel).toHaveBeenCalled();
   }));
 });
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 fd45fdbc..6bd22a01 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,5 +1,6 @@
-import { Component, ViewChild } from '@angular/core';
+import { Component, Input, 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';
@@ -27,9 +28,9 @@ export class CardComponent extends AbstractViewComponent<CardView> {
     this.body = this.v.config.body;
   }
 
-  onActionSubmit() {
-    if (this.appForm?.formGroup && this.appForm?.formGroup.value) {
-      this.actionService.perform(this.appForm.formGroup.value);
+  onActionSubmit(formGroup: FormGroup) {
+    if (formGroup && formGroup.value) {
+      this.actionService.perform(formGroup.value);
       this.currentAction = undefined;
     }
   }
diff --git a/web/src/app/modules/shared/components/presentation/code/code.component.html b/web/src/app/modules/shared/components/presentation/code/code.component.html
index a2e2728f..af365e60 100644
--- a/web/src/app/modules/shared/components/presentation/code/code.component.html
+++ b/web/src/app/modules/shared/components/presentation/code/code.component.html
@@ -5,7 +5,7 @@
     title="Copy to clipboard"
     (click)="copyToClipboard(value)"
   >
-    <clr-icon shape="copy-to-clipboard" size="28"></clr-icon>
+    <clr-icon shape="copy-to-clipboard" size="32"></clr-icon>
   </button>
 </div>
 <pre>
diff --git a/web/src/app/modules/shared/components/presentation/code/code.component.scss b/web/src/app/modules/shared/components/presentation/code/code.component.scss
index 721e8e93..17ea2821 100644
--- a/web/src/app/modules/shared/components/presentation/code/code.component.scss
+++ b/web/src/app/modules/shared/components/presentation/code/code.component.scss
@@ -4,10 +4,6 @@
 
 .code-button-wrapper {
   position: absolute;
-  margin-top: 10px;
+  margin-top: 20px;
   right: 30px;
 }
-
-pre {
-  min-height: 40px;
-}
diff --git a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.html b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.html
index 1d19b107..24c940f6 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
@@ -3,9 +3,9 @@
   *ngIf="showTitle() || buttonGroup"
 >
   <h4 class="clr-col-10" *ngIf="showTitle()">{{ title }}</h4>
-    <clr-dg-action-bar class="clr-col-2" *ngIf="buttonGroup">
-      <app-button-group [view]="buttonGroup"></app-button-group>
-    </clr-dg-action-bar>
+  <clr-dg-action-bar class="clr-col-2" *ngIf="buttonGroup">
+    <app-button-group class="btn-sm" [view]="buttonGroup"></app-button-group>
+  </clr-dg-action-bar>
 </div>
 <clr-datagrid [clrDgLoading]="false">
   <clr-dg-placeholder>
diff --git a/web/src/app/modules/shared/components/presentation/form/form.component.html b/web/src/app/modules/shared/components/presentation/form/form.component.html
index 8d12a22a..ae464816 100644
--- a/web/src/app/modules/shared/components/presentation/form/form.component.html
+++ b/web/src/app/modules/shared/components/presentation/form/form.component.html
@@ -1,91 +1,95 @@
-<form clrForm [formGroup]="formGroup" >
-  <ng-container *ngFor="let field of 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>
+<form clrForm [formGroup]="formGroup" (ngSubmit)="onFormSubmit()">
+  <div class="card">
+    <div class="card-block">
+      <h3 class="card-title">{{ title }}</h3>
+      <ng-container *ngFor="let field of form.fields">
+        <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-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-radio-container>
+          </ng-container>
+          <ng-container *ngSwitchCase="'text'">
+            <clr-input-container>
+              <label [for]="field.name">{{ field.label }}</label>
+              <input clrInput type="text" [formControlName]="field.name" />
+            </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-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-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-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-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>
-      <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" [formControlName]="field.name" />
-          <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>
+    </div>
+    <div class="card-footer">
+      <button class="btn btn-primary btn-sm" type="submit">Submit</button>
+      <button class="btn btn-sm" type="button" (click)="onFormCancel()">
+        Cancel
+      </button>
+    </div>
+  </div>
 </form>
diff --git a/web/src/app/modules/shared/components/presentation/form/form.component.spec.ts b/web/src/app/modules/shared/components/presentation/form/form.component.spec.ts
index 91fcfaea..f3bff5c1 100644
--- a/web/src/app/modules/shared/components/presentation/form/form.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/form/form.component.spec.ts
@@ -25,6 +25,8 @@ describe('FormComponent', () => {
     component.form = {
       fields: [],
     };
+    component.title = 'Title';
+
     fixture.detectChanges();
   });
 
diff --git a/web/src/app/modules/shared/components/presentation/form/form.component.ts b/web/src/app/modules/shared/components/presentation/form/form.component.ts
index f0cb7053..1dc94169 100644
--- a/web/src/app/modules/shared/components/presentation/form/form.component.ts
+++ b/web/src/app/modules/shared/components/presentation/form/form.component.ts
@@ -2,13 +2,13 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component, Input, OnInit, Output } from '@angular/core';
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import { ActionField, ActionForm } from '../../../models/content';
 import {
+  AbstractControl,
   FormBuilder,
+  FormControl,
   FormGroup,
-  ValidatorFn,
-  Validators,
 } from '@angular/forms';
 
 interface Choice {
@@ -26,40 +26,44 @@ export class FormComponent implements OnInit {
   @Input()
   form: ActionForm;
 
+  @Input()
+  title: string;
+
+  @Output()
+  submit: EventEmitter<FormGroup> = new EventEmitter(true);
+
+  @Output()
+  cancel: EventEmitter<boolean> = new EventEmitter(true);
+
   formGroup: FormGroup;
 
   constructor(private formBuilder: FormBuilder) {}
 
   ngOnInit() {
     if (this.form) {
-      const controls: { [name: string]: any } = {};
+      const controls: { [name: string]: AbstractControl } = {};
       this.form.fields.forEach(field => {
-        controls[field.name] = [
-          field.value,
-          this.getValidators(field.validators),
-        ];
+        const value = field.value;
+        controls[field.name] = new FormControl(value);
       });
 
       this.formGroup = this.formBuilder.group(controls);
     }
   }
 
-  getValidators(validators: string[]): ValidatorFn[] {
-    if (validators) {
-      const vFn: ValidatorFn[] = [];
-      validators.forEach(v => {
-        vFn.push(Validators[v]);
-      });
-      return vFn;
-    }
-    return [];
+  onFormSubmit() {
+    this.submit.emit(this.formGroup);
+  }
+
+  onFormCancel() {
+    this.cancel.emit(true);
   }
 
   fieldChoices(field: ActionField) {
     return field.configuration.choices as Choice[];
   }
 
-  trackByFn(index, _) {
+  trackByFn(index, item) {
     return index;
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/modal/modal.component.html b/web/src/app/modules/shared/components/presentation/modal/modal.component.html
deleted file mode 100644
index cf443237..00000000
--- a/web/src/app/modules/shared/components/presentation/modal/modal.component.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<clr-modal [(clrModalOpen)]="opened" [clrModalStaticBackdrop]="false" [clrModalClosable]="true" [clrModalSize]="size">
-  <h3 class="modal-title">
-    <app-view-title [views]="title"></app-view-title>
-  </h3>
-  <div class="modal-body">
-    <app-view-container [view]="body"></app-view-container>
-    <app-form *ngIf="form?.fields" #modalAppForm [form]="form"></app-form>
-  </div>
-  <div class="modal-footer">
-    <ng-container *ngIf="buttons">
-      <ng-container *ngFor="let button of buttons; trackBy: trackByFn; last as isLast">
-        <button type="button" class="btn" [ngClass]="(isLast)?'btn-primary':'btn-outline'" (click)="onClick(button.payload)">
-          {{ button.name }}
-        </button>
-      </ng-container>
-    </ng-container>
-    <ng-container *ngIf="form?.fields">
-      <button type="button" class="btn btn-outline" (click)="opened = false">Cancel</button>
-      <button type="button" class="btn btn-primary submit" (click)="onFormSubmit()">Submit</button>
-    </ng-container>
-    <ng-container *ngIf="!buttons && !form?.fields">
-      <button type="button" class="btn btn-primary" (click)="opened = false">
-        Close
-      </button>
-    </ng-container>
-  </div>
-</clr-modal>
diff --git a/web/src/app/modules/shared/components/presentation/modal/modal.component.spec.ts b/web/src/app/modules/shared/components/presentation/modal/modal.component.spec.ts
deleted file mode 100644
index 42dcf154..00000000
--- a/web/src/app/modules/shared/components/presentation/modal/modal.component.spec.ts
+++ /dev/null
@@ -1,31 +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 { ModalComponent } from './modal.component';
-import { SharedModule } from '../../../shared.module';
-import { ModalService } from '../../../services/modal/modal.service';
-
-describe('ModalComponent', () => {
-  let component: ModalComponent;
-  let fixture: ComponentFixture<ModalComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      imports: [SharedModule],
-      declarations: [ModalComponent],
-      providers: [{ provide: ModalService }],
-    }).compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(ModalComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/web/src/app/modules/shared/components/presentation/modal/modal.component.ts b/web/src/app/modules/shared/components/presentation/modal/modal.component.ts
deleted file mode 100644
index 62348088..00000000
--- a/web/src/app/modules/shared/components/presentation/modal/modal.component.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
-import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
-import {
-  TitleView,
-  ModalView,
-  View,
-  ActionForm,
-  Button,
-} from '../../../models/content';
-import { FormComponent } from '../form/form.component';
-import { ModalService } from '../../../services/modal/modal.service';
-import { Subscription } from 'rxjs';
-import { WebsocketService } from '../../../services/websocket/websocket.service';
-import { ActionService } from '../../../services/action/action.service';
-
-interface Choice {
-  label: string;
-  value: string;
-  checked: boolean;
-}
-
-@Component({
-  selector: 'app-view-modal',
-  templateUrl: './modal.component.html',
-  styleUrls: ['./modal.component.scss'],
-})
-export class ModalComponent
-  extends AbstractViewComponent<ModalView>
-  implements OnInit, OnDestroy {
-  @ViewChild('modalAppForm') modalAppForm: FormComponent;
-
-  title: TitleView[];
-  body: View;
-  form: ActionForm;
-  opened = false;
-  size: string;
-  action: string;
-  buttons: Button[];
-
-  private modalSubscription: Subscription;
-
-  constructor(
-    private actionService: ActionService,
-    private modalService: ModalService,
-    private websocketService: WebsocketService
-  ) {
-    super();
-  }
-
-  ngOnInit() {
-    this.modalSubscription = this.modalService.isOpened.subscribe(isOpened => {
-      this.opened = isOpened;
-    });
-  }
-
-  ngOnDestroy(): void {
-    if (this.modalSubscription) {
-      this.modalSubscription.unsubscribe();
-    }
-  }
-
-  update() {
-    this.title = this.v.metadata.title as TitleView[];
-    this.body = this.v.config.body;
-    this.size = this.v.config.size;
-    this.form = this.v.config.form;
-    this.opened = this.v.config.opened;
-    this.modalService.setState(this.opened);
-    this.action = this.form?.action;
-    this.buttons = this.v.config.buttons;
-  }
-
-  onFormSubmit() {
-    if (this.modalAppForm && this.modalAppForm.formGroup.valid) {
-      this.websocketService.sendMessage('action.octant.dev/performAction', {
-        action: this.action,
-        formGroup: this.modalAppForm.formGroup.value,
-      });
-      this.opened = false;
-    }
-  }
-
-  onClick(payload: {}) {
-    this.actionService.perform(payload);
-    this.opened = false;
-  }
-}
diff --git a/web/src/app/modules/shared/components/presentation/modal/modal.component.scss b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.sass
similarity index 100%
rename from web/src/app/modules/shared/components/presentation/modal/modal.component.scss
rename to web/src/app/modules/shared/components/presentation/stepper/stepper.component.sass
diff --git a/web/src/app/modules/shared/components/presentation/stepper/stepper.component.scss b/web/src/app/modules/shared/components/presentation/stepper/stepper.component.scss
deleted file mode 100644
index e69de29b..00000000
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 7835d6a3..b6fa83ee 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
@@ -18,7 +18,7 @@ interface Choice {
 @Component({
   selector: 'app-stepper',
   templateUrl: './stepper.component.html',
-  styleUrls: ['./stepper.component.scss'],
+  styleUrls: ['./stepper.component.sass'],
 })
 export class StepperComponent extends AbstractViewComponent<StepperView> {
   @Output()
diff --git a/web/src/app/modules/shared/components/presentation/summary/summary.component.html b/web/src/app/modules/shared/components/presentation/summary/summary.component.html
index 550d3033..a468add0 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
@@ -1,63 +1,45 @@
-<div class="card">
-  <div class="progress loop" *ngIf="isLoading">
-    <progress></progress>
-  </div>
-  <div class="card-block">
-    <ng-container *ngTemplateOutlet="currentAction ? action : content">
-    </ng-container>
-  </div>
-  <ng-container *ngTemplateOutlet="currentAction ? formFooter :  contentFooter"></ng-container>
-</div>
+<ng-container *ngTemplateOutlet="currentAction ? action : content">
+</ng-container>
 
 <ng-template #action>
   <app-form
-    #appForm
     [form]="currentAction.form"
+    [title]="currentAction.title"
+    (submit)="onActionSubmit($event)"
+    (cancel)="onActionCancel()"
   >
   </app-form>
 </ng-template>
 
 <ng-template #content>
-  <div class="card-block">
-    <h3 class="card-title">{{ title }}</h3>
+  <div class="card">
+    <div class="progress loop" *ngIf="isLoading">
+      <progress></progress>
+    </div>
+    <div class="card-block">
+      <h3 class="card-title">{{ title }}</h3>
 
-    <app-alert *ngIf="v?.config.alert" [alert]="v.config.alert"></app-alert>
+      <app-alert *ngIf="v?.config.alert" [alert]="v.config.alert"></app-alert>
 
-    <table class="table-noborder">
-      <tbody>
-        <tr *ngFor="let item of v?.config.sections; trackBy: identifyItem">
-          <td class="left">{{ item.header }}</td>
-          <td class="left">
-            <app-view-table *ngIf="item.content?.metadata?.type === 'table'; else dynamic"
-              [view]="item.content"
-            ></app-view-table>
-            <ng-template #dynamic>
+      <table class="table-noborder">
+        <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>
-            </ng-template>
-          </td>
-        </tr>
-      </tbody>
-    </table>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    <div class="card-footer" *ngIf="shouldShowFooter()">
+      <ng-container
+        *ngFor="let action of v.config.actions; trackBy: identifyItem"
+      >
+        <button class="btn btn-sm btn-link" (click)="setAction(action)">
+          {{ action.name }}
+        </button>
+      </ng-container>
+    </div>
   </div>
 </ng-template>
-
-<ng-template #contentFooter>
-  <div class="card-footer" *ngIf="shouldShowFooter()">
-    <ng-container
-      *ngFor="let action of v.config.actions; trackBy: identifyItem"
-    >
-      <button class="btn btn-sm btn-link" (click)="setAction(action)">
-        {{ action.name }}
-      </button>
-    </ng-container>
-  </div>
-</ng-template>
-
-<ng-template #formFooter>
-  <div class="card-footer">
-    <button class="btn btn-primary btn-sm" type="submit" (click)="onActionSubmit()">Submit</button>
-    <button class="btn btn-sm" type="button" (click)="onActionCancel()">
-      Cancel
-    </button>
-  </div>
-</ng-template>
\ No newline at end of file
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 fa303fb5..7900bc58 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,7 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import { Component, ViewChild } from '@angular/core';
+import { Component } from '@angular/core';
 import {
   Action,
   SummaryItem,
@@ -10,7 +10,6 @@ import {
 } from 'src/app/modules/shared/models/content';
 import { FormGroup } from '@angular/forms';
 import { ActionService } from '../../../services/action/action.service';
-import { FormComponent } from '../form/form.component';
 import { ViewService } from '../../../services/view/view.service';
 import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 
@@ -20,8 +19,6 @@ import { AbstractViewComponent } from '../../abstract-view/abstract-view.compone
   styleUrls: ['./summary.component.scss'],
 })
 export class SummaryComponent extends AbstractViewComponent<SummaryView> {
-  @ViewChild('appForm') appForm: FormComponent;
-
   title: string;
   isLoading = false;
 
@@ -51,9 +48,9 @@ export class SummaryComponent extends AbstractViewComponent<SummaryView> {
     this.currentAction = action;
   }
 
-  onActionSubmit() {
-    if (this.appForm?.formGroup && this.appForm?.formGroup.value) {
-      this.actionService.perform(this.appForm.formGroup.value);
+  onActionSubmit(formGroup: FormGroup) {
+    if (formGroup && formGroup.value) {
+      this.actionService.perform(formGroup.value);
       this.currentAction = undefined;
     }
   }
diff --git a/web/src/app/modules/shared/components/smart/editor/editor.component.scss b/web/src/app/modules/shared/components/smart/editor/editor.component.scss
index b8ce41f7..f6a3b574 100644
--- a/web/src/app/modules/shared/components/smart/editor/editor.component.scss
+++ b/web/src/app/modules/shared/components/smart/editor/editor.component.scss
@@ -1,14 +1,6 @@
 /* Copyright (c) 2020 the Octant contributors. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
-:host-context(body) {
-  --editor-border-color: #d8e3e9;
-}
-
-// Color variables for Dark Theme.
-:host-context(body.dark) {
-  --editor-border-color: #324f62;
-}
 
 .editor-container {
   height: 90%;
@@ -18,8 +10,8 @@
 
 .editor {
   flex: 1;
+  height: 0;
   margin: 1.25em 0;
-  border: 1px solid var(--editor-border-color);
 }
 
 ng-monaco-editor {
diff --git a/web/src/app/modules/shared/components/smart/editor/editor.component.ts b/web/src/app/modules/shared/components/smart/editor/editor.component.ts
index 70354fc2..b578b781 100644
--- a/web/src/app/modules/shared/components/smart/editor/editor.component.ts
+++ b/web/src/app/modules/shared/components/smart/editor/editor.component.ts
@@ -48,9 +48,6 @@ export class EditorComponent extends AbstractViewComponent<EditorView> {
     private actionService: ActionService
   ) {
     super();
-
-    this.uri =
-      'file:text-' + Math.random().toString(36).substring(2, 15) + '.yaml';
   }
 
   update() {
@@ -62,6 +59,8 @@ export class EditorComponent extends AbstractViewComponent<EditorView> {
       this.pristineValue = view.config.value;
       this.options.readOnly = view.config.readOnly;
       this.options.language = view.config.language;
+      this.uri =
+        'file:text-' + Math.random().toString(36).substring(2, 15) + '.yaml';
     }
 
     this.submitAction = view.config.submitAction || this.submitAction;
diff --git a/web/src/app/modules/shared/components/smart/helper/helper.component.html b/web/src/app/modules/shared/components/smart/helper/helper.component.html
index 42b1cc8c..537ef016 100644
--- a/web/src/app/modules/shared/components/smart/helper/helper.component.html
+++ b/web/src/app/modules/shared/components/smart/helper/helper.component.html
@@ -26,11 +26,7 @@
   </clr-dropdown>
 </div>
 
-<clr-modal
-  [(clrModalOpen)]="isBuildModalOpen"
-  [clrModalStaticBackdrop]="false"
-  [clrModalSkipAnimation]="true"
->
+<clr-modal [(clrModalOpen)]="isBuildModalOpen" [clrModalStaticBackdrop]="false">
   <h3 class="modal-title">Build Information</h3>
   <div class="modal-body">
     <table class="table table-vertical table-noborder">
@@ -64,7 +60,6 @@
 <clr-modal
   [(clrModalOpen)]="isShortcutModalOpen"
   [clrModalStaticBackdrop]="false"
-  [clrModalSkipAnimation]="true"
 >
   <h3 class="modal-title">Keyboard Shortcuts</h3>
   <div class="modal-body">
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.html b/web/src/app/modules/shared/components/smart/logs/logs.component.html
index d2d96c77..33bdb7eb 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.html
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.html
@@ -12,25 +12,25 @@
           *ngFor="let container of v?.config.containers"
           value="{{ container }}"
         >
-          {{ container === '' ? '[all containers]' : container }}
-        </option>
+          {{ container === '' ? '[all containers]' : container }}</option
+        >
       </select>
     </clr-select-container>
     <clr-select-container class="container-select">
       <label>Since</label>
       <select
-        clrSelect
-        name="options"
-        [value]="selectedSince"
-        (change)="onSinceChange($event.target.value)"
+              clrSelect
+              name="options"
+              [value]="selectedSince"
+              (change)="onSinceChange($event.target.value)"
       >
         <option
-          *ngFor="let duration of v?.config.durations"
-          label="{{ duration.label }}"
-          value="{{ duration.seconds }}"
+                *ngFor="let duration of v?.config.durations"
+                label="{{ duration.label }}"
+                value="{{ duration.seconds }}"
+        >
+          {{ container === '' ? '[all containers]' : container }}</option
         >
-          {{ container === '' ? '[all containers]' : container }}
-        </option>
       </select>
     </clr-select-container>
     <div class="clr-filter">
@@ -93,7 +93,9 @@
   </div>
   <div class="container-logs">
     <div class="container-logs-bg" #scrollTarget (scroll)="onScroll($event)">
-      <ng-container *ngIf="containerLogs?.length < 1"> No logs </ng-container>
+      <ng-container *ngIf="containerLogs?.length < 1">
+        No logs
+      </ng-container>
       <div
         class="container-log code language-bash"
         *ngFor="let log of filterFunction(containerLogs); trackBy: identifyLog"
diff --git a/web/src/app/modules/shared/components/smart/logs/logs.component.ts b/web/src/app/modules/shared/components/smart/logs/logs.component.ts
index afc3f971..74da1d3f 100644
--- a/web/src/app/modules/shared/components/smart/logs/logs.component.ts
+++ b/web/src/app/modules/shared/components/smart/logs/logs.component.ts
@@ -77,8 +77,8 @@ export class LogsComponent
     }
   }
 
-  onSinceChange(selectedSince: string): void {
-    this.selectedSince = +selectedSince;
+  onSinceChange(sinceSelection: number): void {
+    this.selectedSince = sinceSelection;
     this.stopStreamIfStarted();
     this.startStream();
   }
diff --git a/web/src/app/modules/shared/dynamic-components.ts b/web/src/app/modules/shared/dynamic-components.ts
index 106d1292..b1af6c26 100644
--- a/web/src/app/modules/shared/dynamic-components.ts
+++ b/web/src/app/modules/shared/dynamic-components.ts
@@ -37,7 +37,6 @@ import { SummaryComponent } from './components/presentation/summary/summary.comp
 import { StepperComponent } from './components/presentation/stepper/stepper.component';
 import { TimestampComponent } from './components/presentation/timestamp/timestamp.component';
 import { ContainersComponent } from './components/presentation/containers/containers.component';
-import { ModalComponent } from './components/presentation/modal/modal.component';
 
 export interface ComponentMapping {
   [key: string]: Type<any>;
@@ -63,7 +62,6 @@ const DynamicComponentMapping: ComponentMapping = {
   link: LinkComponent,
   list: ListComponent,
   logs: LogsComponent,
-  modal: ModalComponent,
   podStatus: PodStatusComponent,
   portforward: PortForwardComponent,
   ports: PortsComponent,
diff --git a/web/src/app/modules/shared/models/content.ts b/web/src/app/modules/shared/models/content.ts
index 9984b693..330acac3 100644
--- a/web/src/app/modules/shared/models/content.ts
+++ b/web/src/app/modules/shared/models/content.ts
@@ -109,7 +109,6 @@ export interface Button {
   payload: {};
   name: string;
   confirmation?: Confirmation;
-  modal?: ModalView;
 }
 
 export interface ButtonGroupView extends View {
@@ -186,16 +185,6 @@ export interface SingleStatView extends View {
   };
 }
 
-export interface ModalView extends View {
-  config: {
-    body?: View;
-    opened: boolean;
-    size?: string;
-    form?: ActionForm;
-    buttons?: Button[];
-  };
-}
-
 export interface PodSummary {
   details: View[];
   status: string;
@@ -290,7 +279,6 @@ export interface ActionField {
 
 export interface ActionForm {
   fields: ActionField[];
-  action?: string;
 }
 
 export interface Action {
diff --git a/web/src/app/modules/shared/services/content/content.service.ts b/web/src/app/modules/shared/services/content/content.service.ts
index 761f2b99..889e8a2a 100644
--- a/web/src/app/modules/shared/services/content/content.service.ts
+++ b/web/src/app/modules/shared/services/content/content.service.ts
@@ -35,6 +35,7 @@ const emptyContentResponse: ContentResponse = {
   providedIn: 'root',
 })
 export class ContentService {
+  defaultPath = new BehaviorSubject<string>('');
   current = new BehaviorSubject<ContentResponse>(emptyContentResponse);
   viewScrollPos = new BehaviorSubject<number>(0);
   debouncedScrollPos = new BehaviorSubject<number>(0);
diff --git a/web/src/app/modules/shared/services/loading/loading.service.spec.ts b/web/src/app/modules/shared/services/loading/loading.service.spec.ts
index 4fa9100c..52d8dacf 100644
--- a/web/src/app/modules/shared/services/loading/loading.service.spec.ts
+++ b/web/src/app/modules/shared/services/loading/loading.service.spec.ts
@@ -14,7 +14,7 @@ describe('LoadingService', () => {
   );
 
   it('should be created', () => {
-    const service: LoadingService = TestBed.inject(LoadingService);
+    const service: LoadingService = TestBed.get(LoadingService);
     expect(service).toBeTruthy();
   });
 });
diff --git a/web/src/app/modules/shared/services/modal/modal.service.spec.ts b/web/src/app/modules/shared/services/modal/modal.service.spec.ts
deleted file mode 100644
index 8c0025b5..00000000
--- a/web/src/app/modules/shared/services/modal/modal.service.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (c) 2020 the Octant contributors. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import { TestBed } from '@angular/core/testing';
-import { ModalService } from './modal.service';
-
-describe('ModalService', () => {
-  beforeEach(() =>
-    TestBed.configureTestingModule({
-      providers: [ModalService],
-    })
-  );
-
-  it('should be created', () => {
-    const service: ModalService = TestBed.inject(ModalService);
-    expect(service).toBeTruthy();
-  });
-});
diff --git a/web/src/app/modules/shared/services/modal/modal.service.ts b/web/src/app/modules/shared/services/modal/modal.service.ts
deleted file mode 100644
index e06e0f0f..00000000
--- a/web/src/app/modules/shared/services/modal/modal.service.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (c) 2020 the Octant contributors. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-//
-
-import { Injectable } from '@angular/core';
-import { BehaviorSubject } from 'rxjs';
-
-@Injectable({
-  providedIn: 'root',
-})
-export class ModalService {
-  private modalOpened = new BehaviorSubject<boolean>(false);
-  isOpened = this.modalOpened.asObservable();
-
-  constructor() {}
-
-  openModal() {
-    this.modalOpened.next(true);
-  }
-
-  closeModal() {
-    this.modalOpened.next(false);
-  }
-
-  setState(opened: boolean) {
-    this.modalOpened.next(opened);
-  }
-}
diff --git a/web/src/app/modules/shared/services/namespace/namespace.service.ts b/web/src/app/modules/shared/services/namespace/namespace.service.ts
index e9d7c554..decac872 100644
--- a/web/src/app/modules/shared/services/namespace/namespace.service.ts
+++ b/web/src/app/modules/shared/services/namespace/namespace.service.ts
@@ -41,11 +41,9 @@ export class NamespaceService {
     });
 
     this.activeNamespace.subscribe(namespace => {
-      if (namespace.length > 0) {
-        websocketService.sendMessage('action.octant.dev/setNamespace', {
-          namespace,
-        });
-      }
+      websocketService.sendMessage('action.octant.dev/setNamespace', {
+        namespace,
+      });
     });
 
     this.notifierSession = this.notifierService.createSession();
diff --git a/web/src/app/modules/shared/services/navigation/navigation.service.spec.ts b/web/src/app/modules/shared/services/navigation/navigation.service.spec.ts
index 30e6cee6..783c528b 100644
--- a/web/src/app/modules/shared/services/navigation/navigation.service.spec.ts
+++ b/web/src/app/modules/shared/services/navigation/navigation.service.spec.ts
@@ -50,6 +50,9 @@ describe('NavigationService', () => {
 
         backendService.triggerHandler('event.octant.dev/navigation', update);
         svc.current.subscribe(current => expect(current).toEqual(update));
+        contentService.defaultPath.subscribe(defaultPath =>
+          expect(defaultPath).toEqual(update.defaultPath)
+        );
       }
     ));
 
diff --git a/web/src/app/modules/shared/services/navigation/navigation.service.ts b/web/src/app/modules/shared/services/navigation/navigation.service.ts
index 9a1b55d2..92049e9e 100644
--- a/web/src/app/modules/shared/services/navigation/navigation.service.ts
+++ b/web/src/app/modules/shared/services/navigation/navigation.service.ts
@@ -11,7 +11,6 @@ import { ContentService } from '../content/content.service';
 import { NavigationEnd, Router, RouterEvent } from '@angular/router';
 import { filter } from 'rxjs/operators';
 import { LoadingService } from '../loading/loading.service';
-import { NamespaceService } from '../namespace/namespace.service';
 
 const emptyNavigation: Navigation = {
   sections: [],
@@ -41,6 +40,8 @@ export class NavigationService {
     websocketService.registerHandler('event.octant.dev/navigation', data => {
       const update = data as Navigation;
       this.current.next(update);
+
+      contentService.defaultPath.next(update.defaultPath);
       this.updateLastSelection();
     });
 
diff --git a/web/src/app/modules/shared/shared.module.ts b/web/src/app/modules/shared/shared.module.ts
index 849c7ffd..062e2888 100644
--- a/web/src/app/modules/shared/shared.module.ts
+++ b/web/src/app/modules/shared/shared.module.ts
@@ -32,7 +32,6 @@ import { TimestampComponent } from './components/presentation/timestamp/timestam
 import { LoadingComponent } from './components/presentation/loading/loading.component';
 import { HighlightModule } from 'ngx-highlightjs';
 import { LabelSelectorComponent } from './components/presentation/label-selector/label-selector.component';
-import { ModalComponent } from './components/presentation/modal/modal.component';
 import { CytoscapeComponent } from './components/presentation/cytoscape/cytoscape.component';
 import { Cytoscape2Component } from './components/presentation/cytoscape2/cytoscape2.component';
 import { SelectorsComponent } from './components/presentation/selectors/selectors.component';
@@ -116,7 +115,6 @@ import { BottomPanelComponent } from './components/smart/bottom-panel/bottom-pan
     ListComponent,
     LoadingComponent,
     LogsComponent,
-    ModalComponent,
     ObjectStatusComponent,
     PodStatusComponent,
     PortForwardComponent,
@@ -185,7 +183,6 @@ import { BottomPanelComponent } from './components/smart/bottom-panel/bottom-pan
     ListComponent,
     LoadingComponent,
     LogsComponent,
-    ModalComponent,
     ObjectStatusComponent,
     PodStatusComponent,
     PortForwardComponent,
@@ -262,7 +259,6 @@ import { BottomPanelComponent } from './components/smart/bottom-panel/bottom-pan
     ListComponent,
     LoadingComponent,
     LogsComponent,
-    ModalComponent,
     ObjectStatusComponent,
     PodStatusComponent,
     PortForwardComponent,
diff --git a/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.html b/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.html
index 0bd047c5..6bc92240 100644
--- a/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.html
@@ -3,13 +3,7 @@
   <span>Apply YAML</span>
 </div>
 
-<clr-modal
-  [(clrModalOpen)]="isOpen"
-  [clrModalSize]="'xl'"
-  [clrModalClosable]="true"
-  [clrModalStaticBackdrop]="false"
-  [clrModalSkipAnimation]="true"
->
+<clr-modal [(clrModalOpen)]="isOpen" [clrModalSize]="'xl'" [clrModalClosable]="true" [clrModalStaticBackdrop]="false">
   <h3 class="modal-title">Apply YAML</h3>
   <div class="modal-body">
       <app-view-editor [view]="editorView"></app-view-editor>
diff --git a/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.scss b/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.scss
index 57995474..b3feb706 100644
--- a/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/apply-yaml/apply-yaml.component.scss
@@ -3,8 +3,6 @@
  */
 
 .modal-body {
-  overflow-y: hidden;
-
   ::ng-deep ng-monaco-editor {
     height: calc(60vh - 100px);
   }
diff --git a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.html b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.html
index 3f0a0b24..25187b2a 100644
--- a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.html
@@ -1,5 +1,5 @@
 <div class="app-namespace-selector">
-  <ng-template [ngIf]="showDropdown" [ngIfElse]="hideDropdown">
+  <ng-template [ngIf]="showDropdown()" [ngIfElse]="hideDropdown">
     <clr-dropdown class="dropdown-top" [clrCloseMenuOnItemClick]="true">
       <button type="button" class="dropdown-button" clrDropdownTrigger>
         <clr-icon shape="namespace"></clr-icon>
@@ -21,7 +21,6 @@
             [ngClass]="namespaceClass(namespace)"
             clrDropdownItem
             (click)="selectNamespace(namespace)"
-            [routerLink]="'/overview/namespace/' + namespace"
           >
             {{ namespace }}
           </button>
diff --git a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.ts b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.ts
index 528fcc3c..a49afdd9 100644
--- a/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/namespace/namespace.component.ts
@@ -2,13 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 //
 
-import {
-  ChangeDetectionStrategy,
-  ChangeDetectorRef,
-  Component,
-  OnDestroy,
-  OnInit,
-} from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
 import { NamespaceService } from 'src/app/modules/shared/services/namespace/namespace.service';
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
 import { Subscription } from 'rxjs';
@@ -18,7 +12,6 @@ import { NavigationService } from '../../../../shared/services/navigation/naviga
   selector: 'app-namespace',
   templateUrl: './namespace.component.html',
   styleUrls: ['./namespace.component.scss'],
-  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class NamespaceComponent implements OnInit, OnDestroy {
   namespaces: string[];
@@ -30,46 +23,36 @@ export class NamespaceComponent implements OnInit, OnDestroy {
   };
   lastSelection: number;
   activeUrl: string;
-  showDropdown: boolean;
 
   private namespaceSubscription: Subscription;
 
   constructor(
     private namespaceService: NamespaceService,
-    private navigationService: NavigationService,
-    private cdr: ChangeDetectorRef
+    private navigationService: NavigationService
   ) {}
 
   ngOnInit() {
     this.namespaceSubscription = this.namespaceService.activeNamespace.subscribe(
       (namespace: string) => {
         this.currentNamespace = namespace;
-        this.cdr.detectChanges();
       }
     );
 
     this.namespaceSubscription = this.namespaceService.availableNamespaces.subscribe(
       (namespaces: string[]) => {
         this.namespaces = namespaces;
-        this.cdr.detectChanges();
       }
     );
 
-    this.navigationService.current.subscribe(navigation => {
-      this.navigation = navigation;
-      this.cdr.detectChanges();
-    });
-
-    this.navigationService.activeUrl.subscribe(url => {
-      this.activeUrl = url;
-      this.cdr.detectChanges();
-    });
-
-    this.navigationService.lastSelection.subscribe(selection => {
-      this.lastSelection = selection;
-      this.showDropdown = this.hasDropdown();
-      this.cdr.detectChanges();
-    });
+    this.navigationService.current.subscribe(
+      navigation => (this.navigation = navigation)
+    );
+
+    this.navigationService.lastSelection.subscribe(
+      selection => (this.lastSelection = selection)
+    );
+
+    this.navigationService.activeUrl.subscribe(url => (this.activeUrl = url));
   }
 
   ngOnDestroy(): void {
@@ -85,7 +68,7 @@ export class NamespaceComponent implements OnInit, OnDestroy {
     this.namespaceService.setNamespace(namespace);
   }
 
-  hasDropdown(): boolean {
+  showDropdown() {
     if (this.lastSelection && this.navigation.sections[this.lastSelection]) {
       const url = this.navigation.sections[this.lastSelection].path;
       return !this.isClusterSpecific(url);
@@ -93,32 +76,27 @@ export class NamespaceComponent implements OnInit, OnDestroy {
     return true;
   }
 
-  private namespaceFromUrl(url: string): string {
-    if (url) {
-      const paths = url.split('/');
-      const len = paths.length;
-      if (len > 1 && paths[len - 2] === 'namespaces') {
-        const hashIndex = paths[len - 1].indexOf('#');
-        return hashIndex > 0
-          ? paths[len - 1].substring(0, hashIndex)
-          : paths[len - 1];
-      }
+  private namespaceFromUrl(): string {
+    const paths = this.activeUrl.split('/');
+    const len = paths.length;
+    if (len > 1 && paths[len - 2] === 'namespaces') {
+      const hashIndex = paths[len - 1].indexOf('#');
+      return hashIndex > 0
+        ? paths[len - 1].substring(0, hashIndex)
+        : paths[len - 1];
     }
     return '';
   }
 
   private isClusterSpecific(url: string) {
-    const ns = this.namespaceFromUrl(url);
+    const ns = this.namespaceFromUrl();
     if (ns.length > 0) {
       this.selectNamespace(ns);
       return false;
     }
 
     if (url.includes('cluster-overview')) {
-      return (
-        this.currentNamespace.length === 0 ||
-        !url.endsWith(this.currentNamespace)
-      );
+      return !this.activeUrl.endsWith(this.currentNamespace);
     }
 
     return false;
diff --git a/web/src/app/modules/sugarloaf/components/smart/quick-switcher/quick-switcher.component.scss b/web/src/app/modules/sugarloaf/components/smart/quick-switcher/quick-switcher.component.scss
index d6391f04..752bcb03 100644
--- a/web/src/app/modules/sugarloaf/components/smart/quick-switcher/quick-switcher.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/quick-switcher/quick-switcher.component.scss
@@ -69,10 +69,6 @@
     color: var(--destinationActive-color);
   }
 
-  .modal-body {
-    overflow-y: hidden;
-  }
-
   .modal-footer {
     padding-top: 10px;
     justify-content: flex-start;
diff --git a/web/src/stories/modal.stories.mdx b/web/src/stories/modal.stories.mdx
deleted file mode 100644
index 1c587adc..00000000
--- a/web/src/stories/modal.stories.mdx
+++ /dev/null
@@ -1,121 +0,0 @@
-import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs/blocks';
-import { object, withKnobs } from '@storybook/addon-knobs';
-import { ModalComponent } from '../app/modules/shared/components/presentation/modal/modal.component';
-
-export const modalDocs = { source: { code: `modal := component.NewModal(component.TitleFromString("Modal Title"))
-  modal.SetBody(component.NewText("Modal Body"))
-  modal.SetSize(component.ModalSizeLarge)
-  modal.Open()`
-}}
-
-export const modalFormDocs = { source: { code: `modal := component.NewModal(component.TitleFromString("Modal Title"))
-  modal.SetBody(component.NewText("Modal Body"))
-  fft := component.NewFormFieldText("textFormLabel", "textFormName", "")
-	fft.AddValidator("placeholder", "this is an error", []string{"required"})
-  form := component.Form{
-    Fields: []component.FormField{
-      fft,
-    },
-  }
-  modal.AddForm(form)
-  modal.SetSize(component.ModalSizeLarge)
-  modal.Open()`
-}}
-
-export const textView = {
-  config: {
-    value: 'Modal Title',
-  },
-  metadata: {
-    type: 'text',
-  },
-};
-
-export const bodyView = {
-  config: {
-    value: 'Modal body',
-  },
-  metadata: {
-    type: 'text',
-  },
-};
-
-export const modalView = {
-  metadata: {
-    type: 'modal',
-    title: [textView],
-  },
-  config: {
-    body: bodyView,
-    opened: true,
-    size: 'lg',
-  },
-};
-
-export const formFields = {
-  fields: [
-    {
-      configuration: {},
-      error: 'this is an error',
-      label: 'textFormLabel',
-      name: 'textFormName',
-      placeholder: 'placeholder',
-      type: 'text',
-      validators: ["required"],
-      value: '',
-    },
-  ],
-};
-
-export const modalFormView = {
-  metadata: {
-    type: 'modal',
-    title: [textView],
-  },
-  config: {
-    body: bodyView,
-    form: formFields,
-    opened: true,
-    size: 'lg',
-  },
-};
-
-<h1>Modal component</h1>
-<h2>Description</h2>
-
-<p>The modal component provides a modal</p>
-<h2>Example</h2>
-
-<Meta title="Components/Modal" component={ModalComponent} />
-
-<Canvas withToolbar>
-<Story name="Modal component" parameters={{ docs: modalDocs }} height="500px" >
-  {{
-    props: {
-      view: object('View', modalView),
-    },
-    component: ModalComponent,
-  }}
-</Story>
-</Canvas>
-
-<h2>Props</h2>
-<ArgsTable of={ModalComponent} />
-
-<p>
-Modal Component with a form
-</p>
-
-<Canvas withToolbar>
-  <Story name="Modal component with form" parameters={{ docs: modalFormDocs }} height="500px" >
-  {{
-      props: {
-        view: object('View', modalFormView),
-      },
-      component: ModalComponent,
-  }}
-  </Story>
-</Canvas>
-
-<h2>Props</h2>
-<ArgsTable of={ModalComponent} />
\ No newline at end of file
diff --git a/web/tsconfig.compodoc.json b/web/tsconfig.compodoc.json
index 2179a401..cbbea671 100644
--- a/web/tsconfig.compodoc.json
+++ b/web/tsconfig.compodoc.json
@@ -160,8 +160,6 @@
     "src//app/modules/shared/components/presentation/containers/containers.component.ts",
     "src//app/modules/shared/components/presentation/text/text.component.spec.ts",
     "src//app/modules/shared/components/presentation/text/text.component.ts",
-    "src//app/modules/shared/components/presentation/modal/modal.component.spec.ts",
-    "src//app/modules/shared/components/presentation/modal/modal.component.ts",
     "src//app/modules/shared/components/presentation/breadcrumb/breadcrumb.component.spec.ts",
     "src//app/modules/shared/components/presentation/breadcrumb/breadcrumb.component.ts",
     "src//app/modules/shared/components/presentation/timestamp/timestamp.component.spec.ts",


Test patch:
diff --git a/pkg/action/payload_test.go b/pkg/action/payload_test.go
index 90a2b253..6231a4cc 100644
--- a/pkg/action/payload_test.go
+++ b/pkg/action/payload_test.go
@@ -6,7 +6,6 @@ SPDX-License-Identifier: Apache-2.0
 package action
 
 import (
-	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -239,61 +238,6 @@ func TestPayload_Uint16(t *testing.T) {
 	}
 }
 
-func TestPayload_Int64(t *testing.T) {
-	tests := []struct {
-		name     string
-		payload  Payload
-		key      string
-		isErr    bool
-		expected int64
-	}{
-		{
-			name:     "source is int",
-			payload:  Payload{"int64": float64(7)},
-			key:      "int64",
-			expected: int64(7),
-		},
-		{
-			name:    "source overflows",
-			payload: Payload{"int64": float64(1 << 64)},
-			key:     "int64",
-			isErr:   true,
-		},
-		{
-			name:    "source overflows",
-			payload: Payload{"int64": float64(-1 << 64)},
-			key:     "int64",
-			isErr:   true,
-		},
-		{
-			name:    "value is not int",
-			payload: Payload{"int64": true},
-			key:     "int64",
-			isErr:   true,
-		},
-		{
-			name:    "key does not exist",
-			payload: Payload{},
-			key:     "invalid",
-			isErr:   true,
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			got, err := test.payload.Int64(test.key)
-			if test.isErr {
-				require.Error(t, err)
-				fmt.Println(got, err)
-				return
-			}
-			require.NoError(t, err)
-
-			assert.Equal(t, test.expected, got)
-		})
-	}
-}
-
 func TestPayload_Raw(t *testing.T) {
 	type args struct {
 		key string
diff --git a/pkg/view/component/modal_test.go b/pkg/view/component/modal_test.go
deleted file mode 100644
index e3a67132..00000000
--- a/pkg/view/component/modal_test.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package component
-
-import (
-	"testing"
-)
-
-func TestModal_SetBody(t *testing.T) {
-	modal := NewModal(TitleFromString("modal"))
-	body := NewText("body")
-	modal.SetBody(body)
-
-	expected := NewModal(TitleFromString("modal"))
-	expected.Config.Body = body
-
-	AssertEqual(t, expected, modal)
-}
-
-func TestModal_SetSize(t *testing.T) {
-	tests := []struct {
-		name     string
-		size     ModalSize
-		expected *Modal
-	}{
-		{
-			name: "small",
-			size: ModalSizeSmall,
-			expected: &Modal{
-				Base: newBase(TypeModal, TitleFromString("modal")),
-				Config: ModalConfig{
-					ModalSize: ModalSizeSmall,
-				},
-			},
-		},
-		{
-			name: "large",
-			size: ModalSizeLarge,
-			expected: &Modal{
-				Base: newBase(TypeModal, TitleFromString("modal")),
-				Config: ModalConfig{
-					ModalSize: ModalSizeLarge,
-				},
-			},
-		},
-		{
-			name: "extra large",
-			size: ModalSizeExtraLarge,
-			expected: &Modal{
-				Base: newBase(TypeModal, TitleFromString("modal")),
-				Config: ModalConfig{
-					ModalSize: ModalSizeExtraLarge,
-				},
-			},
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-			modal := NewModal(TitleFromString("modal"))
-			modal.SetSize(test.size)
-
-			AssertEqual(t, test.expected, modal)
-		})
-	}
-}
-
-func TestModal_Open(t *testing.T) {
-	modal := NewModal(TitleFromString("modal"))
-	modal.Open()
-
-	expected := NewModal(TitleFromString("modal"))
-	expected.Config.Opened = true
-	AssertEqual(t, expected, modal)
-}
diff --git a/pkg/view/component/resource_viewer_test.go b/pkg/view/component/resource_viewer_test.go
index d6029402..fb5f2803 100644
--- a/pkg/view/component/resource_viewer_test.go
+++ b/pkg/view/component/resource_viewer_test.go
@@ -52,24 +52,28 @@ func Test_ResourceViewer_Marshal(t *testing.T) {
 							APIVersion: "v1",
 							Kind:       "Service",
 							Status:     "ok",
+							Path:       NewLink("", "my-nginx", "/overview/namespace/default/discovery-and-load-balancing/services/my-nginx"),
 						},
 						"71c2b4eb-2949-11e9-b356-42010a8000e5": Node{
 							Name:       "nginx-deployment",
 							APIVersion: "apps/v1",
 							Kind:       "Deployment",
 							Status:     "ok",
+							Path:       NewLink("", "nginx-deployment", "/overview/namespace/default/workloads/deployments/nginx-deployment"),
 						},
 						"8682460a-29b5-11e9-b356-42010a8000e5": Node{
 							Name:       "nginx-deployment-56c74bb7cd",
 							APIVersion: "extensions/v1beta1",
 							Kind:       "ReplicaSet",
 							Status:     "ok",
+							Path:       NewLink("", "nginx-deployment-56c74bb7cd", "/overview/namespace/default/workloads/replica-sets/nginx-deployment-56c74bb7cd"),
 						},
 						"bf4800b5b6602c4c78ba3b654af02b3b": Node{
 							Name:       "nginx-deployment-56c74bb7cd pods",
 							APIVersion: "v1",
 							Kind:       "Pod",
 							Status:     "ok",
+							Path:       NewLink("", "nginx-deployment-56c74bb7cd pods", "/overview/namespace/default/workloads/pods/nginx-deployment-56c74bb7cd pods"),
 						},
 					},
 				},
diff --git a/pkg/view/component/testdata/config_modal.json b/pkg/view/component/testdata/config_modal.json
deleted file mode 100644
index c69824b2..00000000
--- a/pkg/view/component/testdata/config_modal.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "body":{
-    "metadata":{
-      "type":"text"
-    },
-    "config":{
-      "value":"test"
-    }
-  },
-  "opened":true,
-  "size":"sm"
-}
diff --git a/pkg/view/component/testdata/config_resource_viewer.json b/pkg/view/component/testdata/config_resource_viewer.json
index 80acbb36..30275759 100644
--- a/pkg/view/component/testdata/config_resource_viewer.json
+++ b/pkg/view/component/testdata/config_resource_viewer.json
@@ -30,25 +30,85 @@
           "metadata": { "type": "text" },
           "config": { "value": "my-nginx" }
         }
-      ]
+      ],
+      "path": {
+        "config": {
+          "value": "my-nginx",
+          "ref": "/overview/namespace/default/discovery-and-load-balancing/services/my-nginx"
+        },
+        "metadata": {
+          "type": "link",
+          "title": [
+            {
+              "metadata": { "type": "text" },
+              "config": { "value": "" }
+            }
+          ]
+        }
+      }
     },
     "71c2b4eb-2949-11e9-b356-42010a8000e5": {
       "name": "nginx-deployment",
       "apiVersion": "apps/v1",
       "kind": "Deployment",
-      "status": "ok"
+      "status": "ok",
+      "path": {
+        "config": {
+          "value": "nginx-deployment",
+          "ref": "/overview/namespace/default/workloads/deployments/nginx-deployment"
+        },
+        "metadata": {
+          "type": "link",
+          "title": [
+            {
+              "metadata": { "type": "text" },
+              "config": { "value": "" }
+            }
+          ]
+        }
+      }
     },
     "8682460a-29b5-11e9-b356-42010a8000e5": {
       "name": "nginx-deployment-56c74bb7cd",
       "apiVersion": "extensions/v1beta1",
       "kind": "ReplicaSet",
-      "status": "ok"
+      "status": "ok",
+      "path": {
+        "config": {
+          "value": "nginx-deployment-56c74bb7cd",
+          "ref": "/overview/namespace/default/workloads/replica-sets/nginx-deployment-56c74bb7cd"
+        },
+        "metadata": {
+          "type": "link",
+          "title": [
+            {
+              "metadata": { "type": "text" },
+              "config": { "value": "" }
+            }
+          ]
+        }
+      }
     },
     "bf4800b5b6602c4c78ba3b654af02b3b": {
       "name": "nginx-deployment-56c74bb7cd pods",
       "apiVersion": "v1",
       "kind": "Pod",
-      "status": "ok"
+      "status": "ok",
+      "path": {
+        "config": {
+          "value": "nginx-deployment-56c74bb7cd pods",
+          "ref": "/overview/namespace/default/workloads/pods/nginx-deployment-56c74bb7cd pods"
+        },
+        "metadata": {
+          "type": "link",
+          "title": [
+            {
+              "metadata": { "type": "text" },
+              "config": { "value": "" }
+            }
+          ]
+        }
+      }
     }
   }
 }
diff --git a/pkg/view/component/testdata/resource_viewer.json b/pkg/view/component/testdata/resource_viewer.json
index f7988850..ad43b0c3 100644
--- a/pkg/view/component/testdata/resource_viewer.json
+++ b/pkg/view/component/testdata/resource_viewer.json
@@ -34,25 +34,85 @@
         "name": "my-nginx",
         "apiVersion": "v1",
         "kind": "Service",
-        "status": "ok"
+        "status": "ok",
+        "path": {
+          "config": {
+            "value": "my-nginx",
+            "ref": "/overview/namespace/default/discovery-and-load-balancing/services/my-nginx"
+          },
+          "metadata": {
+            "type": "link",
+            "title": [
+              {
+                "metadata": { "type": "text" },
+                "config": { "value": "" }
+              }
+            ]
+          }
+        }
       },
       "71c2b4eb-2949-11e9-b356-42010a8000e5": {
         "name": "nginx-deployment",
         "apiVersion": "apps/v1",
         "kind": "Deployment",
-        "status": "ok"
+        "status": "ok",
+        "path": {
+          "config": {
+            "value": "nginx-deployment",
+            "ref": "/overview/namespace/default/workloads/deployments/nginx-deployment"
+          },
+          "metadata": {
+            "type": "link",
+            "title": [
+              {
+                "metadata": { "type": "text" },
+                "config": { "value": "" }
+              }
+            ]
+          }
+        }
       },
       "8682460a-29b5-11e9-b356-42010a8000e5": {
         "name": "nginx-deployment-56c74bb7cd",
         "apiVersion": "extensions/v1beta1",
         "kind": "ReplicaSet",
-        "status": "ok"
+        "status": "ok",
+        "path": {
+          "config": {
+            "value": "nginx-deployment-56c74bb7cd",
+            "ref": "/overview/namespace/default/workloads/replica-sets/nginx-deployment-56c74bb7cd"
+          },
+          "metadata": {
+            "type": "link",
+            "title": [
+              {
+                "metadata": { "type": "text" },
+                "config": { "value": "" }
+              }
+            ]
+          }
+        }
       },
       "bf4800b5b6602c4c78ba3b654af02b3b": {
         "name": "nginx-deployment-56c74bb7cd pods",
         "apiVersion": "v1",
         "kind": "Pod",
-        "status": "ok"
+        "status": "ok",
+        "path": {
+          "config": {
+            "value": "nginx-deployment-56c74bb7cd pods",
+            "ref": "/overview/namespace/default/workloads/pods/nginx-deployment-56c74bb7cd pods"
+          },
+          "metadata": {
+            "type": "link",
+            "title": [
+              {
+                "metadata": { "type": "text" },
+                "config": { "value": "" }
+              }
+            ]
+          }
+        }
       }
     }
   }
diff --git a/pkg/view/component/unmarshal_test.go b/pkg/view/component/unmarshal_test.go
index 762b31de..4603021f 100644
--- a/pkg/view/component/unmarshal_test.go
+++ b/pkg/view/component/unmarshal_test.go
@@ -261,19 +261,6 @@ func Test_unmarshal(t *testing.T) {
 				},
 			},
 		},
-		{
-			name:       "modal",
-			configFile: "config_modal.json",
-			objectType: TypeModal,
-			expected: &Modal{
-				Config: ModalConfig{
-					Body:      NewText("test"),
-					Opened:    true,
-					ModalSize: ModalSizeSmall,
-				},
-				Base: newBase(TypeModal, nil),
-			},
-		},
 		{
 			name:       "quadrant",
 			configFile: "config_quadrant.json",
@@ -323,24 +310,28 @@ func Test_unmarshal(t *testing.T) {
 							Details: []Component{
 								NewText("my-nginx"),
 							},
+							Path: NewLink("", "my-nginx", "/overview/namespace/default/discovery-and-load-balancing/services/my-nginx"),
 						},
 						"71c2b4eb-2949-11e9-b356-42010a8000e5": Node{
 							Name:       "nginx-deployment",
 							APIVersion: "apps/v1",
 							Kind:       "Deployment",
 							Status:     "ok",
+							Path:       NewLink("", "nginx-deployment", "/overview/namespace/default/workloads/deployments/nginx-deployment"),
 						},
 						"8682460a-29b5-11e9-b356-42010a8000e5": Node{
 							Name:       "nginx-deployment-56c74bb7cd",
 							APIVersion: "extensions/v1beta1",
 							Kind:       "ReplicaSet",
 							Status:     "ok",
+							Path:       NewLink("", "nginx-deployment-56c74bb7cd", "/overview/namespace/default/workloads/replica-sets/nginx-deployment-56c74bb7cd"),
 						},
 						"bf4800b5b6602c4c78ba3b654af02b3b": Node{
 							Name:       "nginx-deployment-56c74bb7cd pods",
 							APIVersion: "v1",
 							Kind:       "Pod",
 							Status:     "ok",
+							Path:       NewLink("", "nginx-deployment-56c74bb7cd pods", "/overview/namespace/default/workloads/pods/nginx-deployment-56c74bb7cd pods"),
 						},
 					},
 				},