Repository: vmware-tanzu/octant
Pull request #775 (2020-03-16 19:07:04)

Move terminal to tab
Terminal is now available in separate tab inside the Pod view.

Note: I tried to make changes so each browser window creates a separate terminal session, however that was proven to be pretty hard. Passing the session instance from client to backend is not such a big deal (that is implemented now) but bigger problem is that there is only a single channel used to communicate with pod. That, combined with the fact that terminal is partially created when Pod view is opened and then completely created only when Terminal tab is clicked makes it really hard to keep the track of terminal instances and states. We can attempt to do this in future but it will be much bigger surgery in terminal management code. 


wwitzel3: Please rebase/rewrite the commit history as well. Doesn't need to be a single commit, but remove these merges and off-fix commits

Patch:
diff --git a/internal/describer/object.go b/internal/describer/object.go
index e15771d5..2778cda8 100644
--- a/internal/describer/object.go
+++ b/internal/describer/object.go
@@ -20,6 +20,7 @@ import (
 	"github.com/vmware-tanzu/octant/internal/modules/overview/logviewer"
 	"github.com/vmware-tanzu/octant/internal/modules/overview/yamlviewer"
 	"github.com/vmware-tanzu/octant/internal/octant"
+	"github.com/vmware-tanzu/octant/internal/modules/overview/terminalviewer"
 	"github.com/vmware-tanzu/octant/internal/printer"
 	"github.com/vmware-tanzu/octant/internal/resourceviewer"
 	"github.com/vmware-tanzu/octant/pkg/action"
@@ -70,6 +71,7 @@ func NewObject(c ObjectConfig) *Object {
 		{name: "Resource Viewer", tabFunc: o.addResourceViewerTab},
 		{name: "YAML", tabFunc: o.addYAMLViewerTab},
 		{name: "Logs", tabFunc: o.addLogsTab},
+		{name: "Terminal", tabFunc: o.addTerminalTab},
 	}
 
 	return o
@@ -281,3 +283,19 @@ func (d *Object) addLogsTab(ctx context.Context, object runtime.Object, cr *comp
 
 	return nil
 }
+
+func (d *Object) addTerminalTab(ctx context.Context, object runtime.Object, cr *component.ContentResponse, options Options) error {
+	if isPod(object) {
+		logger := log.From(ctx)
+
+		terminalComponent, err := terminalviewer.ToComponent(ctx, object, options.TerminalManager(), logger)
+		if err != nil {
+			return err
+		}
+
+		terminalComponent.SetAccessor("terminal")
+		cr.Add(terminalComponent)
+	}
+
+	return nil
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 8f389437..daa9935d 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -38,7 +38,6 @@ var _ Interface = (*Generator)(nil)
 // Options are additional options to pass a Generator
 type Options struct {
 	LabelSet               *kLabels.Set
-	ExtensionDescriberFunc func(path, namespace string, options describer.Options) (*component.Extension, error)
 }
 
 // NewGenerator creates a Generator.
@@ -110,13 +109,5 @@ func (g *Generator) Generate(ctx context.Context, contentPath string, opts Optio
 		return component.EmptyContentResponse, err
 	}
 
-	if opts.ExtensionDescriberFunc != nil {
-		extensionContent, err := opts.ExtensionDescriberFunc(contentPath, namespace, options)
-		if err != nil {
-			return component.EmptyContentResponse, err
-		}
-		cResponse.SetExtension(extensionContent)
-	}
-
 	return cResponse, nil
 }
diff --git a/internal/modules/clusteroverview/clusteroverview.go b/internal/modules/clusteroverview/clusteroverview.go
index a8b90d30..05bab279 100644
--- a/internal/modules/clusteroverview/clusteroverview.go
+++ b/internal/modules/clusteroverview/clusteroverview.go
@@ -185,7 +185,6 @@ func (co *ClusterOverview) Navigation(ctx context.Context, namespace string, roo
 			"Nodes":              "nodes",
 			"Persistent Volumes": "persistent-volumes",
 			"Port Forwards":      "port-forward",
-			"Terminals":          "terminal",
 		},
 		EntriesFuncs: map[string]octant.EntriesFunc{
 			"Namespaces":         nil,
@@ -194,7 +193,6 @@ func (co *ClusterOverview) Navigation(ctx context.Context, namespace string, roo
 			"Nodes":              nil,
 			"Persistent Volumes": nil,
 			"Port Forwards":      nil,
-			"Terminals":          nil,
 		},
 		Order: []string{
 			"Namespaces",
@@ -203,7 +201,6 @@ func (co *ClusterOverview) Navigation(ctx context.Context, namespace string, roo
 			"Nodes",
 			"Persistent Volumes",
 			"Port Forwards",
-			"Terminals",
 		},
 	}
 
diff --git a/internal/modules/clusteroverview/objects.go b/internal/modules/clusteroverview/objects.go
index dcc96f54..bebe6a37 100644
--- a/internal/modules/clusteroverview/objects.go
+++ b/internal/modules/clusteroverview/objects.go
@@ -80,7 +80,6 @@ var (
 	})
 
 	portForwardDescriber = NewPortForwardListDescriber()
-	terminalDescriber    = NewTerminalListDescriber()
 
 	rootDescriber = describer.NewSection(
 		"/",
@@ -91,6 +90,5 @@ var (
 		nodesDescriber,
 		persistentVolumeDescriber,
 		portForwardDescriber,
-		terminalDescriber,
 	)
 )
diff --git a/internal/modules/clusteroverview/terminal_describer.go b/internal/modules/clusteroverview/terminal_describer.go
deleted file mode 100644
index 2aafafb8..00000000
--- a/internal/modules/clusteroverview/terminal_describer.go
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-Copyright (c) 2019 the Octant contributors. All Rights Reserved.
-SPDX-License-Identifier: Apache-2.0
-*/
-
-package clusteroverview
-
-import (
-	"context"
-	"fmt"
-	"strconv"
-
-	"github.com/vmware-tanzu/octant/internal/describer"
-	"github.com/vmware-tanzu/octant/pkg/action"
-	"github.com/vmware-tanzu/octant/pkg/view/component"
-)
-
-// TerminalListDescriber describes a list of terminals.
-type TerminalListDescriber struct {
-}
-
-func NewTerminalListDescriber() *TerminalListDescriber {
-	return &TerminalListDescriber{}
-}
-
-var _ describer.Describer = (*TerminalListDescriber)(nil)
-
-// Describe describes a list of port forwards as content
-func (d *TerminalListDescriber) Describe(ctx context.Context, namespace string, options describer.Options) (component.ContentResponse, error) {
-	tm := options.TerminalManager()
-
-	list := component.NewList("Terminals", nil)
-
-	tblCols := component.NewTableCols("Container", "Command", "Active", "ID", "Age", "")
-	tbl := component.NewTable("Terminals", "There are no terminals!", tblCols)
-	list.Add(tbl)
-
-	for _, t := range tm.List(namespace) {
-		nameLink, err := options.Link.ForGVK(t.Key().Namespace, t.Key().APIVersion, t.Key().Kind, t.Key().Name, t.Key().Name)
-		if err != nil {
-			return component.EmptyContentResponse, err
-		}
-
-		nameLink.Config.Text = t.Container()
-
-		buttonGroup := component.NewButtonGroup()
-		buttonGroup.AddButton(
-			component.NewButton("Delete",
-				action.CreatePayload("overview/deleteTerminal", action.Payload{"terminalID": t.ID()}),
-				component.WithButtonConfirmation(
-					"Delete Terminal",
-					fmt.Sprintf("Are you sure you want to delete *Terminal* **%s**? You will lose access to the scrollback buffer.", t.ID()),
-				)))
-
-		tRow := component.TableRow{
-			"Container": nameLink,
-			"Command":   component.NewText(t.Command()),
-			"Active":    component.NewText(strconv.FormatBool(t.Active())),
-			"ID":        component.NewText(t.ID()),
-			"Age":       component.NewTimestamp(t.CreatedAt()),
-			"":          buttonGroup,
-		}
-		tbl.Add(tRow)
-	}
-
-	return component.ContentResponse{
-		Components: []component.Component{list},
-	}, nil
-}
-
-func (d *TerminalListDescriber) PathFilters() []describer.PathFilter {
-	filter := describer.NewPathFilter("/terminal", d)
-	return []describer.PathFilter{*filter}
-}
-
-func (d *TerminalListDescriber) Reset(ctx context.Context) error {
-	return nil
-}
diff --git a/internal/modules/overview/overview.go b/internal/modules/overview/overview.go
index 3d70cdf6..52333e5f 100644
--- a/internal/modules/overview/overview.go
+++ b/internal/modules/overview/overview.go
@@ -252,48 +252,10 @@ func (co *Overview) Content(ctx context.Context, contentPath string, opts module
 	ctx = internalLog.WithLoggerContext(ctx, co.dashConfig.Logger())
 	genOpts := generator.Options{
 		LabelSet:               opts.LabelSet,
-		ExtensionDescriberFunc: co.extensionDescriber,
 	}
 	return co.generator.Generate(ctx, contentPath, genOpts)
 }
 
-func (co *Overview) extensionDescriber(path, namespace string, options describer.Options) (*component.Extension, error) {
-	extension := component.NewExtension()
-	terminals := options.TerminalManager().List(namespace)
-
-	for _, t := range terminals {
-		name := t.Command()
-		if !t.Active() {
-			name += " (exited)"
-		}
-		tfl := component.NewFlexLayout(name)
-		tfl.SetAccessor(t.ID())
-
-		details := component.TerminalDetails{
-			Container: t.Container(),
-			Command:   t.Command(),
-			UUID:      t.ID(),
-			CreatedAt: t.CreatedAt(),
-			Active:    t.Active(),
-		}
-
-		tfl.AddSections([]component.FlexLayoutItem{
-			{
-				Width: component.WidthFull,
-				View:  component.NewTerminal(t.Key().Namespace, t.Key().Name, details),
-			},
-		})
-
-		extensionTab := component.ExtensionTab{
-			Tab:          tfl,
-			ClosePayload: action.CreatePayload("overview/deleteTerminal", action.Payload{"terminalID": t.ID()}),
-		}
-
-		extension.AddTab(extensionTab)
-	}
-	return extension, nil
-}
-
 // ActionPaths contain the actions this module is responsible for.
 func (co *Overview) ActionPaths() map[string]action.DispatcherFunc {
 	dispatchers := action.Dispatchers{
diff --git a/internal/modules/overview/terminalviewer/terminalviewer.go b/internal/modules/overview/terminalviewer/terminalviewer.go
new file mode 100644
index 00000000..ce2e52c1
--- /dev/null
+++ b/internal/modules/overview/terminalviewer/terminalviewer.go
@@ -0,0 +1,127 @@
+/*
+Copyright (c) 2019 the Octant contributors. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package terminalviewer
+
+import (
+	"context"
+	"github.com/vmware-tanzu/octant/internal/terminal"
+	"github.com/vmware-tanzu/octant/pkg/log"
+	"github.com/vmware-tanzu/octant/pkg/store"
+	"github.com/vmware-tanzu/octant/pkg/view/component"
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// ToComponent converts an object into a terminal component.
+func ToComponent(ctx context.Context, object runtime.Object, terminalManager terminal.Manager, logger log.Logger) (*component.Terminal, error) {
+	yv, err := new(ctx, object, terminalManager, logger )
+	if err != nil {
+		return nil, errors.Wrap(err, "create Terminal viewer")
+	}
+
+	return yv.ToComponent()
+}
+
+// Terminal Viewer is a terminal viewer for objects.
+type terminalViewer struct {
+	object runtime.Object
+	terminalManager terminal.Manager
+	context context.Context
+	logger log.Logger
+}
+
+// New creates an instance of TerminalViewer.
+func new(context context.Context, object runtime.Object, terminalManager terminal.Manager, logger log.Logger) (*terminalViewer, error) {
+	if object == nil {
+		return nil, errors.New("can't create Terminal view for nil object")
+	}
+
+	return &terminalViewer{
+		context: context,
+		object: object,
+		terminalManager: terminalManager,
+		logger: logger.With("Terminal Viewer", context),
+	}, nil
+}
+
+// ToComponent converts the Terminal Viewer to a component.
+func (tv *terminalViewer) ToComponent() (*component.Terminal, error) {
+	pod, err := getPod(tv)
+	if err != nil {
+		return nil, err
+	}
+
+	key, err:=  store.KeyFromObject(tv.object)
+	if err != nil {
+		return nil, err
+	}
+
+	container:= ""
+	if len(pod.Spec.Containers) > 0 {
+		container= getFirstContainer(pod).Name
+	}
+	t, err := tv.terminalManager.Create(context.Background(), tv.logger, key, container, "/bin/sh", "")
+
+	if err != nil {
+		return nil, err
+	}
+
+	id := ""
+	if t != nil {
+		id= t.ID()
+	}
+
+	details := component.TerminalDetails{
+		Container: container,
+		Command:   "/bin/sh",
+		UUID:      id,
+		Active:    true,
+	}
+	term := component.NewTerminal(pod.Namespace, "Terminal", pod.Name, details)
+	return term, nil
+}
+
+func getFirstContainer(pod *corev1.Pod) corev1.Container {
+	var selected = pod.Spec.Containers[0]
+
+	for _, c := range pod.Spec.Containers {
+		var isInit = false
+		for _, d := range pod.Spec.InitContainers {
+			if d.Name == c.Name {
+				isInit= true
+				break
+			}
+		}
+		if !isInit {
+			selected = c
+			break
+		}
+	}
+
+	return selected
+}
+
+func getPod(tv *terminalViewer) (*corev1.Pod, error) {
+	pod := &corev1.Pod{}
+	switch t := tv.object.(type) {
+	case *unstructured.Unstructured:
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(t.Object, pod); err != nil {
+			return nil, err
+		}
+	case *corev1.Pod:
+		pod = t
+	default:
+		pod = nil
+	}
+
+	if pod == nil {
+		return nil, errors.Errorf("can't fetch Terminal from a %T", tv.object)
+	}
+
+	return pod, nil
+}
diff --git a/internal/octant/terminal.go b/internal/octant/terminal.go
index ec893204..340d9a3f 100644
--- a/internal/octant/terminal.go
+++ b/internal/octant/terminal.go
@@ -7,7 +7,6 @@ package octant
 
 import (
 	"context"
-
 	"github.com/pkg/errors"
 	"github.com/vmware-tanzu/octant/internal/terminal"
 	"github.com/vmware-tanzu/octant/pkg/action"
@@ -54,18 +53,18 @@ func (t *TerminalCommandExec) Handle(ctx context.Context, alerter action.Alerter
 	}
 	t.logger.Debugf("%s", key)
 
-	_, err = t.terminalManager.Create(ctx, t.logger, key, request.container, request.command)
+	_, err = t.terminalManager.Create(ctx, t.logger, key, request.container, request.command, request.sessionID)
 	if err != nil {
 		t.logger.Errorf("%s", err)
 		return errors.Wrap(err, "terminal manager create")
 	}
-
 	return nil
 }
 
 type terminalExecRequest struct {
 	container string
 	command   string
+	sessionID string
 	tty       bool
 }
 
@@ -83,5 +82,9 @@ func terminalExecFromPayload(payload action.Payload) (*terminalExecRequest, erro
 		return nil, err
 	}
 
+	t.sessionID, err = payload.String("sessionID")
+	if err != nil {
+		return nil, err
+	}
 	return t, nil
 }
diff --git a/internal/printer/container.go b/internal/printer/container.go
index d8a1d42f..f0eb2fce 100644
--- a/internal/printer/container.go
+++ b/internal/printer/container.go
@@ -72,7 +72,6 @@ func (cc *ContainerConfiguration) Create() (*component.Summary, error) {
 				return nil, errors.Wrapf(err, "get container status for %q", cc.container.Name)
 			}
 		}
-		cc.actionGenerators = append(cc.actionGenerators, terminalCommandExecAction)
 	}
 
 	sections := component.SummarySections{}
@@ -506,22 +505,6 @@ func editContainerAction(owner runtime.Object, container *corev1.Container) (com
 	return action, nil
 }
 
-func terminalCommandExecAction(owner runtime.Object, container *corev1.Container) (component.Action, error) {
-	form, err := component.CreateFormForObject("overview/commandExec", owner,
-		component.NewFormFieldHidden("containerName", container.Name),
-		component.NewFormFieldText("Command", "containerCommand", ""),
-	)
-	if err != nil {
-		return component.Action{}, err
-	}
-	action := component.Action{
-		Name:  "Execute Command",
-		Title: fmt.Sprintf("Execute %s Command", container.Name),
-		Form:  form,
-	}
-	return action, nil
-}
-
 func containersPathForObject(object runtime.Object) ([]string, error) {
 	if object == nil {
 		return nil, errors.New("object is nil")
diff --git a/internal/terminal/fake/mock_instance.go b/internal/terminal/fake/mock_instance.go
index 807411ba..636ee54d 100644
--- a/internal/terminal/fake/mock_instance.go
+++ b/internal/terminal/fake/mock_instance.go
@@ -50,6 +50,32 @@ func (mr *MockInstanceMockRecorder) ID() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockInstance)(nil).ID))
 }
 
+// SessionID mocks base method
+func (m *MockInstance) SessionID() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "SessionID")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// SessionID indicates an expected call of SessionID
+func (mr *MockInstanceMockRecorder) SessionID() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SessionID", reflect.TypeOf((*MockInstance)(nil).SessionID))
+}
+
+// SetSessionID mocks base method
+func (m *MockInstance) SetSessionID(arg0 string) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetSessionID", arg0)
+}
+
+// SetSessionID indicates an expected call of SetSessionID
+func (mr *MockInstanceMockRecorder) SetSessionID(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSessionID", reflect.TypeOf((*MockInstance)(nil).SetSessionID), arg0)
+}
+
 // Key mocks base method
 func (m *MockInstance) Key() store.Key {
 	m.ctrl.T.Helper()
@@ -106,6 +132,18 @@ func (mr *MockInstanceMockRecorder) Scrollback() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scrollback", reflect.TypeOf((*MockInstance)(nil).Scrollback))
 }
 
+// ResetScrollback mocks base method
+func (m *MockInstance) ResetScrollback() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "ResetScrollback")
+}
+
+// ResetScrollback indicates an expected call of ResetScrollback
+func (mr *MockInstanceMockRecorder) ResetScrollback() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetScrollback", reflect.TypeOf((*MockInstance)(nil).ResetScrollback))
+}
+
 // Read mocks base method
 func (m *MockInstance) Read(size int) ([]byte, error) {
 	m.ctrl.T.Helper()
diff --git a/internal/terminal/fake/mock_interface.go b/internal/terminal/fake/mock_interface.go
index 22ffac97..5166c92b 100644
--- a/internal/terminal/fake/mock_interface.go
+++ b/internal/terminal/fake/mock_interface.go
@@ -78,18 +78,18 @@ func (mr *MockManagerMockRecorder) Delete(id interface{}) *gomock.Call {
 }
 
 // Create mocks base method
-func (m *MockManager) Create(ctx context.Context, logger log.Logger, key store.Key, container, command string) (terminal.Instance, error) {
+func (m *MockManager) Create(ctx context.Context, logger log.Logger, key store.Key, container, command, sessionID string) (terminal.Instance, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Create", ctx, logger, key, container, command)
+	ret := m.ctrl.Call(m, "Create", ctx, logger, key, container, command, sessionID)
 	ret0, _ := ret[0].(terminal.Instance)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // Create indicates an expected call of Create
-func (mr *MockManagerMockRecorder) Create(ctx, logger, key, container, command interface{}) *gomock.Call {
+func (mr *MockManagerMockRecorder) Create(ctx, logger, key, container, command, sessionID interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), ctx, logger, key, container, command)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), ctx, logger, key, container, command, sessionID)
 }
 
 // Select mocks base method
diff --git a/internal/terminal/instance.go b/internal/terminal/instance.go
index 5ac88341..a1c441fd 100644
--- a/internal/terminal/instance.go
+++ b/internal/terminal/instance.go
@@ -24,10 +24,13 @@ import (
 // Instance defines the interface to a single exec instance.
 type Instance interface {
 	ID() string
+	SessionID() string
+	SetSessionID(string)
 	Key() store.Key
 	Container() string
 	Command() string
 	Scrollback() []byte
+	ResetScrollback()
 
 	Read(size int) ([]byte, error)
 	Write(key []byte) error
@@ -67,6 +70,7 @@ var _ PTY = (*pty)(nil)
 
 // Write writes bytes to the "stdout/err" buffer.
 func (p *pty) Write(b []byte) (int, error) {
+	p.logger.Debugf("Starting Terminal Instance Write ", b)
 	p.mu.Lock()
 	defer p.mu.Unlock()
 	defer p.activityFunc()
@@ -76,18 +80,22 @@ func (p *pty) Write(b []byte) (int, error) {
 
 // Read reads bytes from stdin (p.keystroke) and copies them to the stdin buffer.
 func (p *pty) Read(b []byte) (int, error) {
+	p.logger.Debugf("Starting PTY Read ")
 	select {
 	case <-p.ctx.Done():
 		p.cancelFn()
 
 		if p.ctx.Err() != nil {
 			if p.ctx.Err() == context.Canceled {
+				p.logger.Debugf("PTY ErrClosedPipe", p.ctx.Err())
 				return 0, io.ErrClosedPipe
 			}
 
+			p.logger.Debugf("PTY ErrUnexpectedEOF ", p.ctx.Err())
 			return 0, io.ErrUnexpectedEOF
 		}
 
+		p.logger.Debugf("PTY ErrClosedPipe", p.ctx.Err())
 		return 0, io.ErrClosedPipe
 	default:
 		break
@@ -117,6 +125,7 @@ func (p *pty) Next() *remotecommand.TerminalSize {
 		return nil
 	}
 
+	p.logger.Debugf("PTY Next ", size)
 	return &size
 }
 
@@ -132,6 +141,7 @@ type instance struct {
 	ctx context.Context
 
 	id        uuid.UUID
+	sessionID string
 	key       store.Key
 	createdAt time.Time
 
@@ -149,7 +159,7 @@ type instance struct {
 var _ Instance = (*instance)(nil)
 
 // NewTerminalInstance creates a concrete Terminal
-func NewTerminalInstance(ctx context.Context, logger log.Logger, key store.Key, container, command string, activityChan chan Instance) Instance {
+func NewTerminalInstance(ctx context.Context, logger log.Logger, key store.Key, container, command string, activityChan chan Instance, sessionID string) Instance {
 	ctx, cancelFn := context.WithCancel(ctx)
 
 	termPty := &pty{
@@ -170,6 +180,7 @@ func NewTerminalInstance(ctx context.Context, logger log.Logger, key store.Key,
 		container: container,
 		command:   command,
 		pty:       termPty,
+		sessionID: sessionID,
 		logger:    logger,
 	}
 
@@ -196,10 +207,12 @@ func (t *instance) Read(size int) ([]byte, error) {
 	}
 
 	buf := make([]byte, size)
+	t.pty.logger.Debugf("Starting Terminal Instance Read ")
 
 	n, err := t.pty.ReadStdout(buf)
 	if err != nil {
 		if err == io.EOF {
+			t.pty.logger.Debugf("Terminal Instance Read received EOF", buf[:n])
 			line := buf[:n]
 			if string(line) == "" {
 				return nil, nil
@@ -220,6 +233,7 @@ func (t *instance) Read(size int) ([]byte, error) {
 	if _, err := t.scrollback.Write(b); err != nil {
 		return nil, err
 	}
+	t.pty.logger.Debugf("Terminal Instance Read completed", b)
 
 	return b, nil
 }
@@ -227,6 +241,8 @@ func (t *instance) Read(size int) ([]byte, error) {
 // Write sends the passed in key to the stdin of the instance.
 // If the instance is not a TTY, Write will return an error.
 func (t *instance) Write(key []byte) error {
+	t.pty.logger.Debugf("Terminal Instance Write", key)
+
 	if t.pty == nil {
 		return errors.New("can not execute command, no stdin")
 	}
@@ -245,6 +261,7 @@ func (t *instance) ExitMessage() string { return t.exitMessage }
 func (t *instance) Active() bool {
 	select {
 	case <-t.ctx.Done():
+		t.pty.logger.Debugf("Terminal Instance received ctx.Done, error is:", t.ctx.Err())
 		return false
 	default:
 		return true
@@ -263,9 +280,18 @@ func (t *instance) Key() store.Key { return t.key }
 // is populated by calling Read.
 func (t *instance) Scrollback() []byte { return t.scrollback.Bytes() }
 
-// ID returns the ID for the termianl. This is a UUID returned as a string.
+// ResetScrollback empties the scrollback buffer
+func (t *instance) ResetScrollback()  { t.scrollback.Reset() }
+
+// ID returns the ID for the terminal. This is a UUID returned as a string.
 func (t *instance) ID() string { return t.id.String() }
 
+// SessionID returns the ID for the browser session. This is a pointer to a string representation of UUID.
+func (t *instance) SessionID() string { return t.sessionID }
+
+// ID returns the ID for the terminal browsersession. This is a UUID returned as a string.
+func (t *instance) SetSessionID(id string) { t.sessionID = id }
+
 // Container returns the container name that the terminal is associated with.
 func (t *instance) Container() string { return t.container }
 
diff --git a/internal/terminal/manager.go b/internal/terminal/manager.go
index f36b7022..2123f820 100644
--- a/internal/terminal/manager.go
+++ b/internal/terminal/manager.go
@@ -31,7 +31,7 @@ type Manager interface {
 	List(namespace string) []Instance
 	Get(id string) (Instance, bool)
 	Delete(id string)
-	Create(ctx context.Context, logger log.Logger, key store.Key, container string, command string) (Instance, error)
+	Create(ctx context.Context, logger log.Logger, key store.Key, container string, command string, sessionID string) (Instance, error)
 	Select(ctx context.Context) chan Instance
 	ForceUpdate(id string)
 	SendScrollback(id string) bool
@@ -92,10 +92,24 @@ func (tm *manager) Select(ctx context.Context) chan Instance {
 	return tm.chanInstance
 }
 
-func (tm *manager) Create(ctx context.Context, logger log.Logger, key store.Key, container, command string) (Instance, error) {
-	t := NewTerminalInstance(ctx, logger, key, container, command, tm.chanInstance)
-	tm.instances.Store(t.ID(), t)
+func (tm *manager) Create(ctx context.Context, logger log.Logger, key store.Key, container string, command string, sessionID string) (Instance, error) {
+	var t, err = tm.Find(key, sessionID)
+
+	if t != nil {
+		logger.Debugf("Using cashed terminal", t.ID(), t.Active())
+		if !t.Active() {
+			logger.Debugf("Deleting terminal", t.ID())
+			t.ResetScrollback()
+			tm.SetScrollback(t.ID(), true)
+			tm.ForceUpdate(t.ID())
+			tm.Delete(t.ID())
+		}
+		return t, err
+	}
 
+	ctx = context.WithValue(ctx, "sessionID", sessionID)
+	t = NewTerminalInstance(ctx, logger, key, container, command, tm.chanInstance, sessionID)
+	tm.instances.Store(t.ID(), t)
 	pod, err := tm.objectStore.Get(ctx, key)
 	if err != nil {
 		return nil, err
@@ -104,6 +118,8 @@ func (tm *manager) Create(ctx context.Context, logger log.Logger, key store.Key,
 		return nil, errors.New("pod not found")
 	}
 
+	logger.Debugf("NewTerminalInstance created for", pod.GetName(), pod.GetUID())
+
 	req := tm.restClient.Post().
 		Resource("pods").
 		Name(pod.GetName()).
@@ -144,7 +160,6 @@ func (tm *manager) Create(ctx context.Context, logger log.Logger, key store.Key,
 		t.Stop()
 		logger.Debugf("stopping stream command")
 	}()
-
 	return t, nil
 }
 
@@ -156,6 +171,18 @@ func (tm *manager) Get(id string) (Instance, bool) {
 	return v.(Instance), ok
 }
 
+func (tm *manager) Find(key store.Key, sessionID string) (Instance, error) {
+	for _, terminalInstance := range tm.List(key.Namespace) {
+		if key == terminalInstance.Key() {
+			if len(terminalInstance.SessionID()) == 0 && len(sessionID) > 0 {
+				terminalInstance.SetSessionID(sessionID)
+			}
+			return terminalInstance, nil
+		}
+	}
+	return nil, errors.New("terminal not found")
+}
+
 func (tm *manager) List(namespace string) []Instance {
 	instances := []Instance{}
 	tm.instances.Range(func(k interface{}, v interface{}) bool {
diff --git a/pkg/view/component/terminal.go b/pkg/view/component/terminal.go
index 0d33b053..41c425dd 100644
--- a/pkg/view/component/terminal.go
+++ b/pkg/view/component/terminal.go
@@ -7,7 +7,6 @@ package component
 
 import (
 	"encoding/json"
-	"fmt"
 	"time"
 )
 
@@ -23,6 +22,7 @@ type TerminalDetails struct {
 type TerminalConfig struct {
 	Namespace string          `json:"namespace"`
 	Name      string          `json:"name"`
+	PodName   string          `json:"podName"`
 	Details   TerminalDetails `json:"terminal"`
 }
 
@@ -32,12 +32,13 @@ type Terminal struct {
 }
 
 // NewTerminal creates a terminal component.
-func NewTerminal(namespace, name string, details TerminalDetails) *Terminal {
+func NewTerminal(namespace, name string, podName string, details TerminalDetails) *Terminal {
 	return &Terminal{
-		base: newBase(typeTerminal, TitleFromString(fmt.Sprintf("%s / %s", namespace, name))),
+		base: newBase(typeTerminal, TitleFromString(name)),
 		Config: TerminalConfig{
 			Namespace: namespace,
 			Name:      name,
+			PodName:   podName,
 			Details:   details,
 		},
 	}
diff --git a/web/package-lock.json b/web/package-lock.json
index e459dce8..6b4ff769 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -314,6 +314,12 @@
           "requires": {
             "glob": "^7.1.3"
           }
+        },
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+          "dev": true
         }
       }
     },
@@ -11591,6 +11597,14 @@
         "tough-cookie": "~2.4.3",
         "tunnel-agent": "^0.6.0",
         "uuid": "^3.3.2"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+          "dev": true
+        }
       }
     },
     "request-promise-core": {
@@ -12493,6 +12507,14 @@
       "requires": {
         "faye-websocket": "^0.10.0",
         "uuid": "^3.0.1"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+          "dev": true
+        }
       }
     },
     "sockjs-client": {
@@ -13737,6 +13759,12 @@
           "requires": {
             "ms": "^2.1.1"
           }
+        },
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+          "dev": true
         }
       }
     },
@@ -14067,10 +14095,9 @@
       "dev": true
     },
     "uuid": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
-      "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
-      "dev": true
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz",
+      "integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw=="
     },
     "validate-npm-package-license": {
       "version": "3.0.4",
@@ -15797,6 +15824,14 @@
       "requires": {
         "ansi-colors": "^3.0.0",
         "uuid": "^3.3.2"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.4.0",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+          "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
+          "dev": true
+        }
       }
     },
     "webpack-merge": {
diff --git a/web/package.json b/web/package.json
index 95835b3c..30532635 100644
--- a/web/package.json
+++ b/web/package.json
@@ -62,6 +62,7 @@
     "rxjs": "~6.5.3",
     "serialize-javascript": ">=2.1.1",
     "tslib": "^1.10.0",
+    "uuid": "^7.0.2",
     "ws": "^7.2.0",
     "xterm": "^4.3.0",
     "xterm-addon-fit": "^0.3.0",
diff --git a/web/src/app/modules/shared/components/smart/terminal/terminal.component.scss b/web/src/app/modules/shared/components/smart/terminal/terminal.component.scss
index b512b0ff..5c9e7f72 100644
--- a/web/src/app/modules/shared/components/smart/terminal/terminal.component.scss
+++ b/web/src/app/modules/shared/components/smart/terminal/terminal.component.scss
@@ -1,9 +1,10 @@
 @import "xterm/css/xterm.css";
 
 .app-terminal {
-  top: 40px;
-  left: 2px;
-  height: calc(100% - 1.5rem);
-  width: calc(100% - 2.5px);
-  position: absolute;
-}
\ No newline at end of file
+  height: 100%;
+  min-height: 20vh;
+
+  border: .05rem solid #ccc;
+  border-radius: .15rem;
+  margin: .6rem 0;
+}
diff --git a/web/src/app/modules/shared/components/smart/terminal/terminal.component.ts b/web/src/app/modules/shared/components/smart/terminal/terminal.component.ts
index 82fa7b74..e4580223 100644
--- a/web/src/app/modules/shared/components/smart/terminal/terminal.component.ts
+++ b/web/src/app/modules/shared/components/smart/terminal/terminal.component.ts
@@ -13,18 +13,15 @@ import {
 } from '@angular/core';
 import { Terminal } from 'xterm';
 import { FitAddon } from 'xterm-addon-fit';
+import { v4 as uuidv4 } from 'uuid';
 import {
   TerminalOutputService,
   TerminalOutputStreamer,
 } from 'src/app/modules/shared/terminals/terminals.service';
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
-import {
-  TerminalDetail,
-  TerminalView,
-} from 'src/app/modules/shared/models/content';
+import { TerminalView } from 'src/app/modules/shared/models/content';
 import { WebsocketService } from '../../../services/websocket/websocket.service';
-import { SliderService } from 'src/app/modules/shared/slider/slider.service';
-import { Subscription } from 'rxjs';
+import { ActionService } from '../../../services/action/action.service';
 
 @Component({
   encapsulation: ViewEncapsulation.None,
@@ -33,12 +30,17 @@ import { Subscription } from 'rxjs';
   templateUrl: './terminal.component.html',
 })
 export class TerminalComponent implements OnDestroy, AfterViewInit {
+  constructor(
+    private terminalService: TerminalOutputService,
+    private wss: WebsocketService,
+    private actionService: ActionService
+  ) {}
+
+  private static sessionID = uuidv4();
   private terminalStream: TerminalOutputStreamer;
   private term: Terminal;
   private fitAddon: FitAddon;
   trackByIdentity = trackByIdentity;
-  sliderServiceSubscription: Subscription;
-
   @Input() view: TerminalView;
   @ViewChild('terminal', { static: true }) terminalDiv: ElementRef;
   @HostListener('click') onClick() {
@@ -46,44 +48,41 @@ export class TerminalComponent implements OnDestroy, AfterViewInit {
     this.fitAddon.fit();
   }
 
-  constructor(
-    private terminalService: TerminalOutputService,
-    private wss: WebsocketService,
-    private sliderService: SliderService
-  ) {}
-
-  compareFn(c1: TerminalDetail, c2: TerminalDetail): boolean {
-    return c1 && c2 ? c1.uuid === c2.uuid : c1 === c2;
-  }
-
   ngOnDestroy(): void {
     if (this.terminalStream) {
       this.terminalStream.scrollback.unsubscribe();
       this.terminalStream.line.unsubscribe();
       this.terminalStream = null;
     }
-
-    if (this.sliderServiceSubscription) {
-      this.sliderServiceSubscription.unsubscribe();
-    }
   }
 
   ngAfterViewInit() {
     if (this.view) {
-      const disableStdin = this.view.config.terminal.active ? false : true;
       const logLevel = 'info';
-
+      const { podName, namespace, terminal } = this.view.config;
+      const { active, uuid, command, container } = terminal;
+      const disableStdin = !active;
+      const terminalCommand = {
+        containerName: container,
+        containerCommand: command,
+        name: podName,
+        namespace,
+        kind: 'Pod',
+        apiVersion: 'v1',
+        sessionID: TerminalComponent.sessionID,
+        action: 'overview/commandExec',
+      };
+      this.actionService.perform(terminalCommand);
       this.term = new Terminal({
         logLevel,
         disableStdin,
       });
-
       this.initStream();
       this.enableResize();
       this.term.onData(data => {
-        if (this.view.config.terminal.active === true) {
+        if (active) {
           this.wss.sendMessage('sendTerminalCommand', {
-            terminalID: this.view.config.terminal.uuid,
+            terminalID: uuid,
             key: data,
           });
         }
@@ -93,58 +92,54 @@ export class TerminalComponent implements OnDestroy, AfterViewInit {
       this.term.open(this.terminalDiv.nativeElement);
       this.term.focus();
       this.fitAddon.fit();
-
-      this.sliderServiceSubscription = this.sliderService.resizedSliderEvent.subscribe(
-        () => {
-          this.term.focus();
-          setTimeout(() => {
-            this.fitAddon.fit();
-          }, 0);
-        }
-      );
+      this.updateTerminalHeight();
     }
   }
 
+  private updateTerminalHeight() {
+    const roundedModulo = this.terminalDiv.nativeElement.clientHeight % 17;
+    this.terminalDiv.nativeElement.style.height = `calc(100% - ${roundedModulo}px)`;
+  }
+
   enableResize() {
     let timeOut = null;
     const resizeDebounce = (e: { cols: number; rows: number }) => {
       const resize = () => {
-        if (this.view.config.terminal.active) {
+        const { active, uuid } = this.view.config.terminal;
+        if (active) {
           this.wss.sendMessage('sendTerminalResize', {
-            terminalID: this.view.config.terminal.uuid,
+            terminalID: uuid,
             rows: e.rows,
             cols: e.cols,
           });
+          this.fitAddon.fit();
         }
       };
 
       if (timeOut != null) {
         clearTimeout(timeOut);
       }
-      timeOut = setTimeout(resize, 1000);
-      this.fitAddon.fit();
+      timeOut = setTimeout(resize, 500);
     };
     this.term.onResize(resizeDebounce);
   }
 
   initStream() {
-    const namespace = this.view.config.namespace;
-    const name = this.view.config.name;
-    const terminal = this.view.config.terminal;
-
-    if (namespace && name && terminal.container && terminal.uuid) {
+    const { namespace, podName, terminal } = this.view.config;
+    const { active, uuid, container } = terminal;
+    if (namespace && podName && container && uuid) {
       this.terminalStream = this.terminalService.createStream(
         namespace,
-        name,
-        terminal.container,
-        terminal.uuid
+        podName,
+        container,
+        uuid
       );
       this.terminalStream.scrollback.subscribe((scrollback: string) => {
         if (scrollback && scrollback.length !== 0) {
           this.term.write(atob(scrollback).replace(/\n/g, '\n\r'));
         }
       });
-      if (terminal.active) {
+      if (active) {
         this.terminalStream.line.subscribe((line: string) => {
           if (line && line.length !== 0) {
             this.term.write(atob(line).replace(/\n/g, '\n\r'));
diff --git a/web/src/app/modules/shared/models/content.ts b/web/src/app/modules/shared/models/content.ts
index 83ec3b47..01fe78fe 100644
--- a/web/src/app/modules/shared/models/content.ts
+++ b/web/src/app/modules/shared/models/content.ts
@@ -395,6 +395,7 @@ export interface TerminalView extends View {
   config: {
     namespace: string;
     name: string;
+    podName: string;
     terminal: TerminalDetail;
   };
 }
diff --git a/web/src/app/modules/sugarloaf/components/smart/container/container.component.html b/web/src/app/modules/sugarloaf/components/smart/container/container.component.html
index 9ded09a3..13ae8ad2 100644
--- a/web/src/app/modules/sugarloaf/components/smart/container/container.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/container/container.component.html
@@ -19,7 +19,7 @@
             <app-context-selector></app-context-selector>
         </div>
     </header>
-    <div class="content-container" [ngStyle]="style">
+    <div class="content-container">
         <div class="navigation">
             <div class="namespace-switcher">
                 <app-namespace></app-namespace>
diff --git a/web/src/app/modules/sugarloaf/components/smart/container/container.component.scss b/web/src/app/modules/sugarloaf/components/smart/container/container.component.scss
index e1af29b1..05fdcad9 100644
--- a/web/src/app/modules/sugarloaf/components/smart/container/container.component.scss
+++ b/web/src/app/modules/sugarloaf/components/smart/container/container.component.scss
@@ -41,8 +41,6 @@ $navigation-width: 13rem;
   }
 
   .content-container {
-    margin-bottom: 1.5rem;
-
     .navigation {
       border-right: 1px solid var(--navigation-border-color);
       overflow-y: auto;
diff --git a/web/src/app/modules/sugarloaf/components/smart/container/container.component.ts b/web/src/app/modules/sugarloaf/components/smart/container/container.component.ts
index adfdc226..7bb2a572 100644
--- a/web/src/app/modules/sugarloaf/components/smart/container/container.component.ts
+++ b/web/src/app/modules/sugarloaf/components/smart/container/container.component.ts
@@ -4,18 +4,10 @@
  *
  */
 
-import {
-  Component,
-  ElementRef,
-  OnDestroy,
-  OnInit,
-  ViewChild,
-} from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
 import { Navigation } from '../../../models/navigation';
 import { WebsocketService } from '../../../../shared/services/websocket/websocket.service';
 import { IconService } from '../../../../shared/services/icon/icon.service';
-import { SliderService } from '../../../../shared/slider/slider.service';
-import { NavigationStart, Router } from '@angular/router';
 
 @Component({
   selector: 'app-root',
@@ -23,32 +15,18 @@ import { NavigationStart, Router } from '@angular/router';
   styleUrls: ['./container.component.scss'],
 })
 export class ContainerComponent implements OnInit, OnDestroy {
-  @ViewChild('scrollTarget') scrollTarget: ElementRef;
   navigation: Navigation;
   style: object = {};
 
   constructor(
     private websocketService: WebsocketService,
-    private iconService: IconService,
-    private sliderService: SliderService,
-    private router: Router
+    private iconService: IconService
   ) {
     iconService.load({
       iconName: 'octant-logo',
       // tslint:disable-next-line:max-line-length
       iconSource: `<svg width="106px" height="126px" viewBox="0 0 106 126" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="288026-vmw-os-lgo-octant-final" fill-rule="nonzero"> <g id="Group"> <circle id="Oval" fill="#FFFFFF" cx="53.4" cy="67.1" r="52.5"></circle> <path d="M58.6,112.7 L58.6,112.7 C58.6,115.5 56.4,117.7 53.6,117.7 L53.6,117.7 C50.8,117.7 48.6,115.5 48.6,112.7 L48.6,112.7 C48.6,110 50.8,107.7 53.6,107.7 L53.6,107.7 C56.4,107.7 58.6,110 58.6,112.7 Z" id="Path" fill="#62BB46"></path> <path d="M53.4,14.6 C24.4,14.6 0.9,38.1 0.9,67.1 C0.9,81 6.3,93.7 15.2,103 C16.6,104.5 18.1,105.8 19.6,107.1 L23.2,97.3 C21.5,95.6 19.9,93.7 18.5,91.7 C13.6,84.7 10.6,76.2 10.6,67 C10.6,43.3 29.8,24.2 53.4,24.2 C77,24.2 96.2,43.4 96.2,67 C96.2,76 93.4,84.3 88.7,91.2 C87.2,93.3 85.6,95.3 83.8,97.1 L87.4,106.9 C89,105.5 90.6,104.1 92,102.5 C100.6,93.2 105.8,80.7 105.8,67 C105.9,38.1 82.4,14.6 53.4,14.6 Z" id="Path" fill="#727175"></path> <path d="M83.8,97.2 L60.5,33.5 L57.2,34.7 L56.6,31.9 L55.4,32.1 L70.2,106.4 C65.9,108.3 61.2,109.4 56.2,109.8 C55.2,109.9 54.3,109.9 53.3,109.9 C52.5,109.9 51.7,109.9 50.9,109.8 C45.8,109.5 41,108.3 36.5,106.4 L51.3,32.1 L50.1,31.9 L49.5,34.7 L46.3,33.5 L22.9,97.3 L19.3,107.1 C19.7,107.5 20.2,107.9 20.6,108.2 L20.6,108.1 L20.6,108.1 L20.6,108.2 C20.6,108.2 20.6,108.2 20.6,108.2 L20.6,108.2 C21.3,108.8 22.1,109.3 22.9,109.9 C24.1,110.7 25.3,111.5 26.6,112.3 L26.6,112.3 C34.4,116.9 43.5,119.6 53.2,119.6 C63.1,119.6 72.4,116.8 80.3,112.1 L80.3,112.1 C81.5,111.4 82.6,110.6 83.7,109.8 C84.6,109.2 85.4,108.6 86.2,107.9 C86.6,107.6 86.9,107.3 87.3,107 L83.8,97.2 Z M29.9,102.9 C28.8,102.1 27.7,101.3 26.6,100.5 L48.6,40.6 L35.5,106 C33.5,105.1 31.7,104.1 29.9,102.9 L29.9,102.9 Z M77.3,102.6 L77.3,102.6 C75.4,103.9 73.5,105 71.4,105.9 L58.3,40 L80.4,100.4 C79.4,101.1 78.4,101.9 77.3,102.6 Z" id="Shape" fill="#00438C"></path> <g transform="translate(43.000000, 8.000000)" id="Path"> <polygon fill="#FFFFFF" points="10.6 0.7 2.3 4.9 0.1 13.9 5.9 21.2 15.1 21.2 21 13.8 18.9 4.9"></polygon> <path d="M15.5,11.3 L15.5,11.3 C15.5,14.1 13.3,16.3 10.5,16.3 L10.5,16.3 C7.7,16.3 5.5,14.1 5.5,11.3 L5.5,11.3 C5.5,8.6 7.7,6.3 10.5,6.3 L10.5,6.3 C13.3,6.3 15.5,8.5 15.5,11.3 Z" fill="#62BB46"></path> </g> <path d="M71.6,23.5 L68,8.1 L53.6,0.9 L39.3,8 L35.5,23.5 L45.6,36.2 L49.9,36.2 L49.9,105.4 L56.9,105.4 L56.9,36.2 L61.5,36.2 L71.6,23.5 Z M48.9,29.2 L43.1,21.9 L45.3,12.9 L53.6,8.8 L61.9,12.9 L64,21.9 L58.1,29.2 L48.9,29.2 Z" id="Shape" fill="#009CDC"></path> <g transform="translate(42.000000, 103.000000)"> <path d="M10.6,6.2 C7.9,6.6 6,9.1 6.4,11.9 C6.8,14.6 9.3,16.5 12.1,16.1 C14.8,15.7 16.7,13.2 16.3,10.4 C15.9,7.6 13.3,5.8 10.6,6.2 Z" id="Path" fill="#FFFFFF"></path> <path d="M22.2,9.5 C21.3,3.5 15.7,-0.7 9.7,0.2 L9.7,0.2 C3.7,1.1 -0.5,6.7 0.4,12.7 C1.2,18.2 5.9,22.1 11.3,22.1 C11.8,22.1 12.4,22.1 12.9,22 C18.9,21.1 23.1,15.5 22.2,9.5 Z M12.1,16 C9.4,16.4 6.8,14.5 6.4,11.8 C6,9.1 7.9,6.5 10.6,6.1 C13.3,5.7 15.9,7.6 16.3,10.3 C16.7,13.1 14.8,15.6 12.1,16 Z" id="Shape" fill="#62BB46"></path> </g> </g> </g> </g> </svg>`,
     });
-    this.sliderService.setHeight$.subscribe((data: number) => {
-      Object.assign(this.style, { marginBottom: `${data}px` });
-      setTimeout(() => {
-        this.sliderService.resizedSliderEvent.emit(true);
-      }, 0);
-    });
-    router.events.subscribe(data => {
-      if (data instanceof NavigationStart) {
-        this.style = {};
-      }
-    });
   }
 
   ngOnInit(): void {
diff --git a/web/src/app/modules/sugarloaf/components/smart/content/content.component.html b/web/src/app/modules/sugarloaf/components/smart/content/content.component.html
index b763a220..6c4cb0d2 100644
--- a/web/src/app/modules/sugarloaf/components/smart/content/content.component.html
+++ b/web/src/app/modules/sugarloaf/components/smart/content/content.component.html
@@ -1,4 +1,4 @@
-<div #scrollTarget class="content-component">
+<div class="content-component">
     <ng-container *ngIf="hasReceivedContent">
         <ng-container *ngIf="hasTabs; then withTabs; else withoutTabs"></ng-container>
         <ng-template #withTabs>
@@ -7,7 +7,6 @@
         <ng-template #withoutTabs>
             <app-content-switcher [view]="singleView"></app-content-switcher>
         </ng-template>
-        <app-slider-view [view]="extView"></app-slider-view>
     </ng-container>
 </div>
 


Test patch:
diff --git a/internal/modules/overview/terminalviewer/terminalviewer_test.go b/internal/modules/overview/terminalviewer/terminalviewer_test.go
new file mode 100644
index 00000000..53138721
--- /dev/null
+++ b/internal/modules/overview/terminalviewer/terminalviewer_test.go
@@ -0,0 +1,40 @@
+/*
+Copyright (c) 2019 the Octant contributors. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+
+package terminalviewer
+
+import (
+	"context"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"github.com/vmware-tanzu/octant/internal/log"
+	terminalFake "github.com/vmware-tanzu/octant/internal/terminal/fake"
+	"github.com/vmware-tanzu/octant/pkg/view/component"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	corev1 "k8s.io/api/core/v1"
+)
+
+func Test_ToComponent(t *testing.T) {
+	controller := gomock.NewController(t)
+	object := &corev1.Pod{}
+	tm := terminalFake.NewMockManager(controller)
+	tm.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),gomock.Any(), gomock.Any()).AnyTimes()
+
+	got, err := ToComponent(context.Background(), object, tm, log.NopLogger())
+	require.NoError(t, err)
+
+	details := component.TerminalDetails{
+		Container: "",
+		Command:   "/bin/sh",
+		UUID:      "",
+		Active:    true,
+	}
+	expected := component.NewTerminal("", "Terminal", "", details)
+
+	assert.Equal(t, expected, got)
+}
diff --git a/internal/printer/container_test.go b/internal/printer/container_test.go
index 5e058a60..d45e1391 100644
--- a/internal/printer/container_test.go
+++ b/internal/printer/container_test.go
@@ -197,21 +197,6 @@ func Test_ContainerConfiguration(t *testing.T) {
 		{
 			name:      "in general",
 			container: validContainer,
-			action: component.Action{
-				Name:  "Execute Command",
-				Title: "Execute nginx Command",
-				Form: component.Form{
-					Fields: []component.FormField{
-						component.NewFormFieldHidden("containerName", "nginx"),
-						component.NewFormFieldText("Command", "containerCommand", ""),
-						component.NewFormFieldHidden("apiVersion", "v1"),
-						component.NewFormFieldHidden("kind", "Pod"),
-						component.NewFormFieldHidden("name", "pod"),
-						component.NewFormFieldHidden("namespace", "namespace"),
-						component.NewFormFieldHidden("action", "overview/commandExec"),
-					},
-				},
-			},
 			expected: component.NewSummary("Container nginx", []component.SummarySection{
 				{
 					Header:  "Image",
@@ -270,21 +255,6 @@ func Test_ContainerConfiguration(t *testing.T) {
 			name:      "init containers",
 			container: validInitContainer,
 			isInit:    true,
-			action: component.Action{
-				Name:  "Execute Command",
-				Title: "Execute busybox Command",
-				Form: component.Form{
-					Fields: []component.FormField{
-						component.NewFormFieldHidden("containerName", "busybox"),
-						component.NewFormFieldText("Command", "containerCommand", ""),
-						component.NewFormFieldHidden("apiVersion", "v1"),
-						component.NewFormFieldHidden("kind", "Pod"),
-						component.NewFormFieldHidden("name", "pod"),
-						component.NewFormFieldHidden("namespace", "namespace"),
-						component.NewFormFieldHidden("action", "overview/commandExec"),
-					},
-				},
-			},
 			expected: component.NewSummary("Init Container busybox", []component.SummarySection{
 				{
 					Header:  "Image",
@@ -401,8 +371,6 @@ func Test_ContainerConfiguration(t *testing.T) {
 			}
 			require.NoError(t, err)
 
-			tc.expected.AddAction(tc.action)
-
 			component.AssertEqual(t, tc.expected, summary)
 		})
 	}
diff --git a/pkg/view/component/terminal_test.go b/pkg/view/component/terminal_test.go
index df4f52d3..0f1b9080 100755
--- a/pkg/view/component/terminal_test.go
+++ b/pkg/view/component/terminal_test.go
@@ -21,7 +21,7 @@ func TestTerminal_Marshal(t *testing.T) {
 		UUID:      "0000-0000-0000-0000-0000",
 		Active:    false,
 	}
-	input := NewTerminal("default", "term-test", details)
+	input := NewTerminal("default", "term-test", "pod-name", details)
 	actual, err := json.Marshal(input)
 	assert.NoError(t, err)
 
diff --git a/pkg/view/component/testdata/terminal.json b/pkg/view/component/testdata/terminal.json
index fc2c8228..766f40f3 100644
--- a/pkg/view/component/testdata/terminal.json
+++ b/pkg/view/component/testdata/terminal.json
@@ -3,13 +3,14 @@
     "type": "terminal",
     "title": [
       {
-        "config": { "value": "default / term-test" },
+        "config": { "value": "term-test" },
         "metadata": { "type": "text" }
       }
     ]
   },
   "config": {
     "name": "term-test",
+    "podName": "pod-name",
     "namespace": "default",
     "terminal": {
       "active": false,