Repository: vmware-tanzu/octant
Pull request #926 (2020-05-07 20:03:01)

Show deleted items with indicators
**What this PR does / why we need it**:

This change does the following:
* Shows items that are being deleted using an animation
* Adds an indicator for containing the object status in lists

This change touches lots of files since we don't have a good way to separate testing the operation list handlers from their contents.

**Release note**:
```
Add indicator to show object status and animation for objects when they are being deleted
```



Patch:
diff --git a/internal/objectstatus/objectstatus.go b/internal/objectstatus/objectstatus.go
index 3044c2ca..def1d975 100644
--- a/internal/objectstatus/objectstatus.go
+++ b/internal/objectstatus/objectstatus.go
@@ -10,6 +10,7 @@ import (
 	"errors"
 	"fmt"
 
+	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/vmware-tanzu/octant/pkg/store"
@@ -84,13 +85,27 @@ func status(ctx context.Context, object runtime.Object, o store.Store, lookup st
 		return ObjectStatus{}, errors.New("object is nil")
 	}
 
+	gvk := object.GetObjectKind().GroupVersionKind()
+	apiVersion, kind := gvk.ToAPIVersionAndKind()
+
+	accessor, err := meta.Accessor(object)
+	if err != nil {
+		return ObjectStatus{}, err
+	}
+
+	if accessor.GetDeletionTimestamp() != nil {
+		return ObjectStatus{
+			nodeStatus: component.NodeStatusWarning,
+			Details: []component.Component{
+				component.NewTextf("%s is being deleted", kind),
+			},
+		}, nil
+	}
+
 	if lookup == nil {
 		return ObjectStatus{}, errors.New("status lookup is nil")
 	}
 
-	gvk := object.GetObjectKind().GroupVersionKind()
-	apiVersion, kind := gvk.ToAPIVersionAndKind()
-
 	fn, ok := lookup[statusKey{apiVersion: apiVersion, kind: kind}]
 	if !ok {
 		return ObjectStatus{
diff --git a/internal/printer/clusterrole.go b/internal/printer/clusterrole.go
index 74d2097c..fbb12614 100644
--- a/internal/printer/clusterrole.go
+++ b/internal/printer/clusterrole.go
@@ -18,13 +18,13 @@ import (
 )
 
 // ClusterRoleListHandler is a printFunc that prints cluster roles
-func ClusterRoleListHandler(_ context.Context, list *rbacv1.ClusterRoleList, options Options) (component.Component, error) {
+func ClusterRoleListHandler(ctx context.Context, list *rbacv1.ClusterRoleList, options Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("cluster role list is nil")
 	}
 
 	cols := component.NewTableCols("Name", "Age")
-	ot := NewObjectTable("Cluster Roles", "We couldn't find any cluster roles!", cols)
+	ot := NewObjectTable("Cluster Roles", "We couldn't find any cluster roles!", cols, options.DashConfig.ObjectStore())
 
 	for _, clusterRole := range list.Items {
 		row := component.TableRow{}
@@ -37,7 +37,7 @@ func ClusterRoleListHandler(_ context.Context, list *rbacv1.ClusterRoleList, opt
 		ts := clusterRole.CreationTimestamp.Time
 		row["Age"] = component.NewTimestamp(ts)
 
-		if err := ot.AddRowForObject(&clusterRole, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &clusterRole, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/clusterrolebinding.go b/internal/printer/clusterrolebinding.go
index 161d8d32..a2250382 100644
--- a/internal/printer/clusterrolebinding.go
+++ b/internal/printer/clusterrolebinding.go
@@ -16,13 +16,13 @@ import (
 )
 
 // ClusterRoleBindingListHandler is a printFunc that prints ClusterRoldBindings
-func ClusterRoleBindingListHandler(_ context.Context, clusterRoleBindingList *rbacv1.ClusterRoleBindingList, options Options) (component.Component, error) {
+func ClusterRoleBindingListHandler(ctx context.Context, clusterRoleBindingList *rbacv1.ClusterRoleBindingList, options Options) (component.Component, error) {
 	if clusterRoleBindingList == nil {
 		return nil, errors.New("cluster role binding list is nil")
 	}
 
 	columns := component.NewTableCols("Name", "Labels", "Age", "Role kind", "Role name")
-	ot := NewObjectTable("Cluster Role Bindings", "We couldn't find any cluster role bindings!", columns)
+	ot := NewObjectTable("Cluster Role Bindings", "We couldn't find any cluster role bindings!", columns, options.DashConfig.ObjectStore())
 
 	for _, roleBinding := range clusterRoleBindingList.Items {
 		row := component.TableRow{}
@@ -45,7 +45,7 @@ func ClusterRoleBindingListHandler(_ context.Context, clusterRoleBindingList *rb
 
 		row["Role name"] = roleName
 
-		if err := ot.AddRowForObject(&roleBinding, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &roleBinding, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/configmap.go b/internal/printer/configmap.go
index 0e52c625..00011f72 100644
--- a/internal/printer/configmap.go
+++ b/internal/printer/configmap.go
@@ -19,14 +19,14 @@ import (
 )
 
 // ConfigMapListHandler is a printFunc that prints ConfigMaps
-func ConfigMapListHandler(_ context.Context, list *corev1.ConfigMapList, opts Options) (component.Component, error) {
+func ConfigMapListHandler(ctx context.Context, list *corev1.ConfigMapList, opts Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("list is nil")
 	}
 
 	// Data column
 	cols := component.NewTableCols("Name", "Labels", "Data", "Age")
-	ot := NewObjectTable("ConfigMaps", "We couldn't find any config maps!", cols)
+	ot := NewObjectTable("ConfigMaps", "We couldn't find any config maps!", cols, opts.DashConfig.ObjectStore())
 
 	for _, c := range list.Items {
 		row := component.TableRow{}
@@ -46,7 +46,7 @@ func ConfigMapListHandler(_ context.Context, list *corev1.ConfigMapList, opts Op
 		ts := c.CreationTimestamp.Time
 		row["Age"] = component.NewTimestamp(ts)
 
-		if err := ot.AddRowForObject(&c, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &c, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/cronjob.go b/internal/printer/cronjob.go
index b6085541..6da5f020 100644
--- a/internal/printer/cronjob.go
+++ b/internal/printer/cronjob.go
@@ -28,7 +28,7 @@ func CronJobListHandler(ctx context.Context, list *batchv1beta1.CronJobList, opt
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Schedule", "Age", "")
-	ot := NewObjectTable("CronJobs", "We couldn't find any cron jobs!", cols)
+	ot := NewObjectTable("CronJobs", "We couldn't find any cron jobs!", cols, opts.DashConfig.ObjectStore())
 
 	for _, c := range list.Items {
 		row := component.TableRow{}
@@ -60,7 +60,7 @@ func CronJobListHandler(ctx context.Context, list *batchv1beta1.CronJobList, opt
 
 		row[""] = buttonGroup
 
-		if err := ot.AddRowForObject(&c, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &c, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/daemonset.go b/internal/printer/daemonset.go
index 982de453..6535cc71 100644
--- a/internal/printer/daemonset.go
+++ b/internal/printer/daemonset.go
@@ -17,14 +17,14 @@ import (
 )
 
 // DaemonSetListHandler is a printFunc that lists daemon sets
-func DaemonSetListHandler(_ context.Context, list *appsv1.DaemonSetList, opts Options) (component.Component, error) {
+func DaemonSetListHandler(ctx context.Context, list *appsv1.DaemonSetList, opts Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("daemon set list is nil")
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Desired", "Current", "Ready",
 		"Up-To-Date", "Age", "Node Selector")
-	ot := NewObjectTable("Daemon Sets", "We couldn't find any daemon sets!", cols)
+	ot := NewObjectTable("Daemon Sets", "We couldn't find any daemon sets!", cols, opts.DashConfig.ObjectStore())
 
 	for _, daemonSet := range list.Items {
 		row := component.TableRow{}
@@ -42,7 +42,7 @@ func DaemonSetListHandler(_ context.Context, list *appsv1.DaemonSetList, opts Op
 		row["Age"] = component.NewTimestamp(daemonSet.ObjectMeta.CreationTimestamp.Time)
 		row["Node Selector"] = printSelectorMap(daemonSet.Spec.Template.Spec.NodeSelector)
 
-		if err := ot.AddRowForObject(&daemonSet, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &daemonSet, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/deployment.go b/internal/printer/deployment.go
index af64f549..df1a1e43 100644
--- a/internal/printer/deployment.go
+++ b/internal/printer/deployment.go
@@ -25,13 +25,13 @@ var (
 )
 
 // DeploymentListHandler is a printFunc that lists deployments
-func DeploymentListHandler(_ context.Context, list *appsv1.DeploymentList, opts Options) (component.Component, error) {
+func DeploymentListHandler(ctx context.Context, list *appsv1.DeploymentList, opts Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("nil list")
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Status", "Age", "Containers", "Selector")
-	ot := NewObjectTable("Deployments", "We couldn't find any deployments!", cols)
+	ot := NewObjectTable("Deployments", "We couldn't find any deployments!", cols, opts.DashConfig.ObjectStore())
 
 	for _, d := range list.Items {
 		row := component.TableRow{}
@@ -57,7 +57,7 @@ func DeploymentListHandler(_ context.Context, list *appsv1.DeploymentList, opts
 		row["Containers"] = containers
 		row["Selector"] = printSelector(d.Spec.Selector)
 
-		if err := ot.AddRowForObject(&d, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &d, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/horizontalpodautoscaler.go b/internal/printer/horizontalpodautoscaler.go
index 7bc0b0dd..70091da1 100644
--- a/internal/printer/horizontalpodautoscaler.go
+++ b/internal/printer/horizontalpodautoscaler.go
@@ -24,14 +24,14 @@ import (
 )
 
 // HorizontalPodAutoscalerListHandler is a printFunc that lists horizontal pod autoscalers
-func HorizontalPodAutoscalerListHandler(_ context.Context, list *autoscalingv1.HorizontalPodAutoscalerList, options Options) (component.Component, error) {
+func HorizontalPodAutoscalerListHandler(ctx context.Context, list *autoscalingv1.HorizontalPodAutoscalerList, options Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("horizontalpod handler list is nil")
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Targets", "Minimum Pods", "Maximum Pods", "Replicas", "Age")
 	ot := NewObjectTable("Horizontal Pod Autoscalers",
-		"We couldn't find any horizontal pod autoscalers", cols)
+		"We couldn't find any horizontal pod autoscalers", cols, options.DashConfig.ObjectStore())
 
 	for _, horizontalPodAutoscaler := range list.Items {
 		row := component.TableRow{}
@@ -58,7 +58,7 @@ func HorizontalPodAutoscalerListHandler(_ context.Context, list *autoscalingv1.H
 		row["Replicas"] = component.NewText(fmt.Sprintf("%d", horizontalPodAutoscaler.Status.CurrentReplicas))
 		row["Age"] = component.NewTimestamp(horizontalPodAutoscaler.CreationTimestamp.Time)
 
-		if err := ot.AddRowForObject(&horizontalPodAutoscaler, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &horizontalPodAutoscaler, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/ingress.go b/internal/printer/ingress.go
index 7168ed29..1521d9e9 100644
--- a/internal/printer/ingress.go
+++ b/internal/printer/ingress.go
@@ -19,13 +19,13 @@ import (
 )
 
 // IngressListHandler is a printFunc that prints ingresses
-func IngressListHandler(_ context.Context, list *extv1beta1.IngressList, options Options) (component.Component, error) {
+func IngressListHandler(ctx context.Context, list *extv1beta1.IngressList, options Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("ingress list is nil")
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Hosts", "Address", "Ports", "Age")
-	ot := NewObjectTable("Ingresses", "We couldn't find any ingresses!", cols)
+	ot := NewObjectTable("Ingresses", "We couldn't find any ingresses!", cols, options.DashConfig.ObjectStore())
 
 	for _, ingress := range list.Items {
 		ports := "80"
@@ -46,7 +46,7 @@ func IngressListHandler(_ context.Context, list *extv1beta1.IngressList, options
 		row["Ports"] = component.NewText(ports)
 		row["Age"] = component.NewTimestamp(ingress.CreationTimestamp.Time)
 
-		if err := ot.AddRowForObject(&ingress, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &ingress, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/job.go b/internal/printer/job.go
index 139d1820..83ba5f03 100644
--- a/internal/printer/job.go
+++ b/internal/printer/job.go
@@ -24,12 +24,12 @@ var (
 )
 
 // JobListHandler prints a job list.
-func JobListHandler(_ context.Context, list *batchv1.JobList, opts Options) (component.Component, error) {
+func JobListHandler(ctx context.Context, list *batchv1.JobList, opts Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("job list is nil")
 	}
 
-	ot := NewObjectTable("Jobs", "We couldn't find any jobs!", JobCols)
+	ot := NewObjectTable("Jobs", "We couldn't find any jobs!", JobCols, opts.DashConfig.ObjectStore())
 
 	for _, job := range list.Items {
 		row := component.TableRow{}
@@ -46,7 +46,7 @@ func JobListHandler(_ context.Context, list *batchv1.JobList, opts Options) (com
 		row["Successful"] = component.NewText(succeeded)
 		row["Age"] = component.NewTimestamp(job.CreationTimestamp.Time)
 
-		if err := ot.AddRowForObject(&job, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &job, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/networkpolicy.go b/internal/printer/networkpolicy.go
index 78445704..c5c9d714 100644
--- a/internal/printer/networkpolicy.go
+++ b/internal/printer/networkpolicy.go
@@ -23,13 +23,13 @@ import (
 )
 
 // NetworkPolicyListHandler is a printFunc that prints network policies
-func NetworkPolicyListHandler(_ context.Context, list *networkingv1.NetworkPolicyList, options Options) (component.Component, error) {
+func NetworkPolicyListHandler(ctx context.Context, list *networkingv1.NetworkPolicyList, options Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("network policy list is nil")
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Age")
-	ot := NewObjectTable("Network Policies", "We couldn't find any network policies!", cols)
+	ot := NewObjectTable("Network Policies", "We couldn't find any network policies!", cols, options.DashConfig.ObjectStore())
 
 	for _, networkPolicy := range list.Items {
 		row := component.TableRow{}
@@ -42,7 +42,7 @@ func NetworkPolicyListHandler(_ context.Context, list *networkingv1.NetworkPolic
 		row["Labels"] = component.NewLabels(networkPolicy.Labels)
 		row["Age"] = component.NewTimestamp(networkPolicy.CreationTimestamp.Time)
 
-		if err := ot.AddRowForObject(&networkPolicy, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &networkPolicy, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/object_table.go b/internal/printer/object_table.go
index 8922bb25..c0e42de3 100644
--- a/internal/printer/object_table.go
+++ b/internal/printer/object_table.go
@@ -6,10 +6,13 @@
 package printer
 
 import (
+	"context"
 	"fmt"
 
+	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/runtime"
 
+	"github.com/vmware-tanzu/octant/internal/objectstatus"
 	"github.com/vmware-tanzu/octant/internal/octant"
 	"github.com/vmware-tanzu/octant/pkg/store"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
@@ -23,15 +26,17 @@ type ObjectTable struct {
 	rows        []component.TableRow
 	filters     map[string]component.TableFilter
 	sortOrder   *tableSetOrder
+	store       store.Store
 }
 
 // NewObjectTable creates an instance of ObjectTable.
-func NewObjectTable(title, placeholder string, cols []component.TableCol) *ObjectTable {
+func NewObjectTable(title, placeholder string, cols []component.TableCol, objectStore store.Store) *ObjectTable {
 	ol := ObjectTable{
 		cols:        cols,
 		title:       title,
 		placeholder: placeholder,
 		filters:     map[string]component.TableFilter{},
+		store:       objectStore,
 	}
 
 	return &ol
@@ -44,13 +49,39 @@ func (ol *ObjectTable) AddFilters(filters map[string]component.TableFilter) {
 	}
 }
 
+type componentStatus interface {
+	SetStatus(status component.TextStatus, detail component.Component)
+}
+
 // AddRowForObject adds a row for an object to the table.
-func (ol *ObjectTable) AddRowForObject(object runtime.Object, row component.TableRow) error {
+func (ol *ObjectTable) AddRowForObject(ctx context.Context, object runtime.Object, row component.TableRow) error {
 	gridAction, err := objectDeleteAction(object)
 	if err != nil {
 		return fmt.Errorf("create object delete action: %w", err)
 	}
 
+	accessor, err := meta.Accessor(object)
+	if err != nil {
+		return fmt.Errorf("get accessor for object: %w", err)
+	}
+
+	if accessor.GetDeletionTimestamp() != nil {
+		row["_isDeleted"] = component.NewText("deleted")
+	}
+
+	status, err := objectstatus.Status(ctx, object, ol.store)
+	if err != nil {
+		return fmt.Errorf("get status for object: %w", err)
+	}
+
+	if len(ol.cols) > 0 {
+		firstRow := row[ol.cols[0].Name]
+		if cs, ok := firstRow.(componentStatus); ok {
+			detailComponent := component.NewList(nil, status.Details)
+			cs.SetStatus(convertNodeStatusToTextStatus(status.Status()), detailComponent)
+		}
+	}
+
 	row.AddAction(gridAction)
 
 	ol.rows = append(ol.rows, row)
@@ -58,6 +89,19 @@ func (ol *ObjectTable) AddRowForObject(object runtime.Object, row component.Tabl
 	return nil
 }
 
+func convertNodeStatusToTextStatus(nodeStatus component.NodeStatus) component.TextStatus {
+	switch nodeStatus {
+	case component.NodeStatusOK:
+		return component.TextStatusOK
+	case component.NodeStatusWarning:
+		return component.TextStatusWarning
+	case component.NodeStatusError:
+		return component.TextStatusError
+	default:
+		return 0
+	}
+}
+
 func objectDeleteAction(object runtime.Object) (component.GridAction, error) {
 	key, err := store.KeyFromObject(object)
 	if err != nil {
diff --git a/internal/printer/persistentvolume.go b/internal/printer/persistentvolume.go
index e09ea77a..448d6ce5 100644
--- a/internal/printer/persistentvolume.go
+++ b/internal/printer/persistentvolume.go
@@ -24,7 +24,7 @@ func PersistentVolumeListHandler(ctx context.Context, list *corev1.PersistentVol
 	}
 
 	cols := component.NewTableCols("Name", "Capacity", "Access Modes", "Reclaim Policy", "Status", "Claim", "Storage Class", "Reason", "Age")
-	ot := NewObjectTable("Persistent Volumes", "We couldn't find any persistent volumes!", cols)
+	ot := NewObjectTable("Persistent Volumes", "We couldn't find any persistent volumes!", cols, options.DashConfig.ObjectStore())
 
 	for _, pv := range list.Items {
 		row := component.TableRow{}
@@ -53,7 +53,7 @@ func PersistentVolumeListHandler(ctx context.Context, list *corev1.PersistentVol
 		row["Reason"] = component.NewText(pv.Status.Reason)
 		row["Age"] = component.NewTimestamp(pv.CreationTimestamp.Time)
 
-		if err := ot.AddRowForObject(&pv, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &pv, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/persistentvolumeclaim.go b/internal/printer/persistentvolumeclaim.go
index c26e4c28..fab4d7a1 100644
--- a/internal/printer/persistentvolumeclaim.go
+++ b/internal/printer/persistentvolumeclaim.go
@@ -26,7 +26,7 @@ func PersistentVolumeClaimListHandler(ctx context.Context, list *corev1.Persiste
 
 	cols := component.NewTableCols("Name", "Status", "Volume", "Capacity", "Access Modes", "Storage Class", "Age")
 	ot := NewObjectTable("Persistent Volume Claims",
-		"We couldn't find any persistent volume claims!", cols)
+		"We couldn't find any persistent volume claims!", cols, options.DashConfig.ObjectStore())
 
 	for _, persistentVolumeClaim := range list.Items {
 		row := component.TableRow{}
@@ -71,7 +71,7 @@ func PersistentVolumeClaimListHandler(ctx context.Context, list *corev1.Persiste
 		ts := persistentVolumeClaim.CreationTimestamp.Time
 		row["Age"] = component.NewTimestamp(ts)
 
-		if err := ot.AddRowForObject(&persistentVolumeClaim, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &persistentVolumeClaim, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/pod.go b/internal/printer/pod.go
index 3e9305ed..ad0d3fcc 100644
--- a/internal/printer/pod.go
+++ b/internal/printer/pod.go
@@ -33,7 +33,7 @@ var (
 )
 
 // PodListHandler is a printFunc that prints pods
-func PodListHandler(_ context.Context, list *corev1.PodList, opts Options) (component.Component, error) {
+func PodListHandler(ctx context.Context, list *corev1.PodList, opts Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("list is nil")
 	}
@@ -43,7 +43,7 @@ func PodListHandler(_ context.Context, list *corev1.PodList, opts Options) (comp
 		cols = podColsWithOutLabels
 	}
 
-	ot := NewObjectTable("Pods", "We couldn't find any pods!", cols)
+	ot := NewObjectTable("Pods", "We couldn't find any pods!", cols, opts.DashConfig.ObjectStore())
 	ot.AddFilters(podTableFilters())
 
 	for i := range list.Items {
@@ -88,7 +88,7 @@ func PodListHandler(_ context.Context, list *corev1.PodList, opts Options) (comp
 		ts := pod.CreationTimestamp.Time
 		row["Age"] = component.NewTimestamp(ts)
 
-		if err := ot.AddRowForObject(&pod, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &pod, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/replicaset.go b/internal/printer/replicaset.go
index 9e3d4b2c..aa71e08d 100644
--- a/internal/printer/replicaset.go
+++ b/internal/printer/replicaset.go
@@ -27,7 +27,7 @@ func ReplicaSetListHandler(ctx context.Context, list *appsv1.ReplicaSetList, opt
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Status", "Age", "Containers", "Selector")
-	ot := NewObjectTable("ReplicaSets", "We couldn't find any replica sets!", cols)
+	ot := NewObjectTable("ReplicaSets", "We couldn't find any replica sets!", cols, opts.DashConfig.ObjectStore())
 
 	for _, rs := range list.Items {
 		row := component.TableRow{}
@@ -52,7 +52,7 @@ func ReplicaSetListHandler(ctx context.Context, list *appsv1.ReplicaSetList, opt
 		row["Containers"] = containers
 		row["Selector"] = printSelector(rs.Spec.Selector)
 
-		if err := ot.AddRowForObject(&rs, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &rs, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/replicationcontroller.go b/internal/printer/replicationcontroller.go
index 56df7217..f3da1cbf 100644
--- a/internal/printer/replicationcontroller.go
+++ b/internal/printer/replicationcontroller.go
@@ -27,7 +27,7 @@ func ReplicationControllerListHandler(ctx context.Context, list *corev1.Replicat
 
 	cols := component.NewTableCols("Name", "Labels", "Status", "Age", "Containers", "Selector")
 	ot := NewObjectTable("ReplicationControllers",
-		"We couldn't find any replication controllers!", cols)
+		"We couldn't find any replication controllers!", cols, options.DashConfig.ObjectStore())
 
 	for _, rc := range list.Items {
 		row := component.TableRow{}
@@ -54,7 +54,7 @@ func ReplicationControllerListHandler(ctx context.Context, list *corev1.Replicat
 
 		row["Selector"] = printSelectorMap(rc.Spec.Selector)
 
-		if err := ot.AddRowForObject(&rc, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &rc, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/role.go b/internal/printer/role.go
index a9e2ba5f..10947a5b 100644
--- a/internal/printer/role.go
+++ b/internal/printer/role.go
@@ -17,13 +17,13 @@ import (
 )
 
 // RoleListHandler is a printFunc that prints roles
-func RoleListHandler(_ context.Context, roleList *rbacv1.RoleList, options Options) (component.Component, error) {
+func RoleListHandler(ctx context.Context, roleList *rbacv1.RoleList, options Options) (component.Component, error) {
 	if roleList == nil {
 		return nil, errors.New("role list is nil")
 	}
 
 	columns := component.NewTableCols("Name", "Age")
-	ot := NewObjectTable("Roles", "We couldn't find any roles!", columns)
+	ot := NewObjectTable("Roles", "We couldn't find any roles!", columns, options.DashConfig.ObjectStore())
 
 	for _, role := range roleList.Items {
 		row := component.TableRow{}
@@ -35,7 +35,7 @@ func RoleListHandler(_ context.Context, roleList *rbacv1.RoleList, options Optio
 		row["Name"] = nameLink
 		row["Age"] = component.NewTimestamp(role.CreationTimestamp.Time)
 
-		if err := ot.AddRowForObject(&role, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &role, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/rolebinding.go b/internal/printer/rolebinding.go
index 90802cb2..843bce2a 100644
--- a/internal/printer/rolebinding.go
+++ b/internal/printer/rolebinding.go
@@ -22,7 +22,7 @@ func RoleBindingListHandler(ctx context.Context, roleBindingList *rbacv1.RoleBin
 	}
 
 	columns := component.NewTableCols("Name", "Age", "Role kind", "Role name")
-	ot := NewObjectTable("Role Bindings", "We couldn't find any role bindings!", columns)
+	ot := NewObjectTable("Role Bindings", "We couldn't find any role bindings!", columns, opts.DashConfig.ObjectStore())
 
 	for _, roleBinding := range roleBindingList.Items {
 		row := component.TableRow{}
@@ -40,7 +40,7 @@ func RoleBindingListHandler(ctx context.Context, roleBindingList *rbacv1.RoleBin
 		}
 		row["Role name"] = roleName
 
-		if err := ot.AddRowForObject(&roleBinding, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &roleBinding, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/secret.go b/internal/printer/secret.go
index 801fcda9..38a812a4 100644
--- a/internal/printer/secret.go
+++ b/internal/printer/secret.go
@@ -26,7 +26,7 @@ func SecretListHandler(ctx context.Context, list *corev1.SecretList, options Opt
 		return nil, errors.New("list of secrets is nil")
 	}
 
-	ot := NewObjectTable("Secrets", "We couldn't find any secrets!", secretTableCols)
+	ot := NewObjectTable("Secrets", "We couldn't find any secrets!", secretTableCols, options.DashConfig.ObjectStore())
 
 	for _, secret := range list.Items {
 		row := component.TableRow{}
@@ -42,7 +42,7 @@ func SecretListHandler(ctx context.Context, list *corev1.SecretList, options Opt
 		row["Data"] = component.NewText(fmt.Sprintf("%d", len(secret.Data)))
 		row["Age"] = component.NewTimestamp(secret.ObjectMeta.CreationTimestamp.Time)
 
-		if err := ot.AddRowForObject(&secret, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &secret, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/service.go b/internal/printer/service.go
index 0916a3e8..2bf41f90 100644
--- a/internal/printer/service.go
+++ b/internal/printer/service.go
@@ -22,13 +22,13 @@ import (
 )
 
 // ServiceListHandler is a printFunc that lists services
-func ServiceListHandler(_ context.Context, list *corev1.ServiceList, options Options) (component.Component, error) {
+func ServiceListHandler(ctx context.Context, list *corev1.ServiceList, options Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("nil list")
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Type", "Cluster IP", "External IP", "Ports", "Age", "Selector")
-	ot := NewObjectTable("Services", "We couldn't find any services!", cols)
+	ot := NewObjectTable("Services", "We couldn't find any services!", cols, options.DashConfig.ObjectStore())
 
 	for _, s := range list.Items {
 		row := component.TableRow{}
@@ -49,7 +49,7 @@ func ServiceListHandler(_ context.Context, list *corev1.ServiceList, options Opt
 
 		row["Selector"] = printSelectorMap(s.Spec.Selector)
 
-		if err := ot.AddRowForObject(&s, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &s, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/serviceaccount.go b/internal/printer/serviceaccount.go
index 25cfae05..76e8aeb2 100644
--- a/internal/printer/serviceaccount.go
+++ b/internal/printer/serviceaccount.go
@@ -21,14 +21,14 @@ import (
 )
 
 // ServiceAccountListHandler is a printFunc that prints service accounts
-func ServiceAccountListHandler(_ context.Context, list *corev1.ServiceAccountList, options Options) (component.Component, error) {
+func ServiceAccountListHandler(ctx context.Context, list *corev1.ServiceAccountList, options Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("service account list is nil")
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Secrets", "Age")
 	ot := NewObjectTable("Service Accounts",
-		"We couldn't find any service accounts!", cols)
+		"We couldn't find any service accounts!", cols, options.DashConfig.ObjectStore())
 
 	for _, serviceAccount := range list.Items {
 		row := component.TableRow{}
@@ -42,7 +42,7 @@ func ServiceAccountListHandler(_ context.Context, list *corev1.ServiceAccountLis
 		row["Secrets"] = component.NewText(fmt.Sprint(len(serviceAccount.Secrets)))
 		row["Age"] = component.NewTimestamp(serviceAccount.CreationTimestamp.Time)
 
-		if err := ot.AddRowForObject(&serviceAccount, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &serviceAccount, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/internal/printer/statefulset.go b/internal/printer/statefulset.go
index 6142e4d2..b85883d4 100644
--- a/internal/printer/statefulset.go
+++ b/internal/printer/statefulset.go
@@ -20,13 +20,13 @@ import (
 )
 
 // StatefulSetListHandler is a printFunc that list stateful sets
-func StatefulSetListHandler(_ context.Context, list *appsv1.StatefulSetList, options Options) (component.Component, error) {
+func StatefulSetListHandler(ctx context.Context, list *appsv1.StatefulSetList, options Options) (component.Component, error) {
 	if list == nil {
 		return nil, errors.New("nil list")
 	}
 
 	cols := component.NewTableCols("Name", "Labels", "Desired", "Current", "Age", "Selector")
-	ot := NewObjectTable("StatefulSets", "We couldn't find any stateful sets!", cols)
+	ot := NewObjectTable("StatefulSets", "We couldn't find any stateful sets!", cols, options.DashConfig.ObjectStore())
 
 	for _, statefulSet := range list.Items {
 		row := component.TableRow{}
@@ -49,7 +49,7 @@ func StatefulSetListHandler(_ context.Context, list *appsv1.StatefulSetList, opt
 
 		row["Selector"] = printSelector(statefulSet.Spec.Selector)
 
-		if err := ot.AddRowForObject(&statefulSet, row); err != nil {
+		if err := ot.AddRowForObject(ctx, &statefulSet, row); err != nil {
 			return nil, fmt.Errorf("add row for object: %w", err)
 		}
 	}
diff --git a/pkg/view/component/link.go b/pkg/view/component/link.go
index e66af42f..57b2061f 100644
--- a/pkg/view/component/link.go
+++ b/pkg/view/component/link.go
@@ -17,17 +17,28 @@ type Link struct {
 type LinkConfig struct {
 	Text string `json:"value"`
 	Ref  string `json:"ref"`
+	// Status sets the status of the component.
+	Status       TextStatus `json:"status,omitempty"`
+	StatusDetail Component  `json:"statusDetail,omitempty"`
 }
 
+type LinkOption func(l *Link)
+
 // NewLink creates a link component
-func NewLink(title, s, ref string) *Link {
-	return &Link{
+func NewLink(title, s, ref string, options ...LinkOption) *Link {
+	l := &Link{
 		base: newBase(typeLink, TitleFromString(title)),
 		Config: LinkConfig{
 			Text: s,
 			Ref:  ref,
 		},
 	}
+
+	for _, option := range options {
+		option(l)
+	}
+
+	return l
 }
 
 // SupportsTitle designates this is a TextComponent.
@@ -48,6 +59,12 @@ func (t *Link) Ref() string {
 	return t.Config.Ref
 }
 
+// SetStatus sets the status of the text component.
+func (t *Link) SetStatus(status TextStatus, detail Component) {
+	t.Config.Status = status
+	t.Config.StatusDetail = detail
+}
+
 type linkMarshal Link
 
 // MarshalJSON implements json.Marshaler
diff --git a/pkg/view/component/text.go b/pkg/view/component/text.go
index add1d202..a15c62b2 100644
--- a/pkg/view/component/text.go
+++ b/pkg/view/component/text.go
@@ -5,7 +5,10 @@ SPDX-License-Identifier: Apache-2.0
 
 package component
 
-import "encoding/json"
+import (
+	"encoding/json"
+	"fmt"
+)
 
 // TextStatus is the status of a text component
 type TextStatus int
@@ -36,13 +39,24 @@ type TextConfig struct {
 }
 
 // NewText creates a text component
-func NewText(s string) *Text {
-	return &Text{
+func NewText(s string, options ...func(*Text)) *Text {
+	t := &Text{
 		base: newBase(typeText, nil),
 		Config: TextConfig{
 			Text: s,
 		},
 	}
+
+	for _, option := range options {
+		option(t)
+	}
+
+	return t
+}
+
+// NewTextf creates a a text component using a printf like helper.
+func NewTextf(format string, a ...interface{}) *Text {
+	return NewText(fmt.Sprintf(format, a...))
 }
 
 // NewMarkdownText creates a text component styled with markdown.
diff --git a/web/package-lock.json b/web/package-lock.json
index 1f0b72be..579e7c53 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1889,9 +1889,9 @@
       }
     },
     "@clr/angular": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@clr/angular/-/angular-3.0.1.tgz",
-      "integrity": "sha512-rVTBHitHb9IWW8lKiKzIRYN6hra6SlpbkKaYp+X7LnBbPYdzjZtwVEH2Az+672pcRZFKcql7DG7G1dQiSKO/xA=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@clr/angular/-/angular-3.1.2.tgz",
+      "integrity": "sha512-tvgU3TxX2U43OMuuRRUEtI1r26KzYwNXdZSkBla/nmDtTaerCxiWWcjU6qhD6/cTq/PQnxkECo3r+BxX9drldw=="
     },
     "@clr/city": {
       "version": "1.1.0",
@@ -1900,9 +1900,9 @@
       "optional": true
     },
     "@clr/core": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@clr/core/-/core-3.0.1.tgz",
-      "integrity": "sha512-eO8mpScH0G5ejklN35L+DPgfAapx8Mi0Zx6nmSC3oz4fIqOqM62F2Lk4/cBR26EzUFA56/RelDP74ZjnJbv4sQ==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@clr/core/-/core-3.1.2.tgz",
+      "integrity": "sha512-+2yCDHUPuhYclCIQc8KqqXzawMyc91UA7X7asMkWn1LrZUeVuvzd2wUT3QhGaKxoXFXqpMnm/mFTEjrzoEKUBA==",
       "requires": {
         "@clr/city": "^1.0.0",
         "@webcomponents/custom-elements": "^1.3.1",
@@ -1916,14 +1916,14 @@
       }
     },
     "@clr/icons": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@clr/icons/-/icons-3.0.1.tgz",
-      "integrity": "sha512-bQrVWe8tYpIwfunIt/IFsCEJ/gL3sbf1jzjyQ3BQFrp0EzqIkcQwb7oBP9I4aaMcUdc5VbS5CV8sYNFDgq2+9g=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@clr/icons/-/icons-3.1.2.tgz",
+      "integrity": "sha512-vd5gFEmS42961wBtnYLvtm2mzSfxJ/V9Mt0aeHJlN9DDQSkq1k5dntYnwYxsjoZeqyMTA6lCDXajEBROBwvr4g=="
     },
     "@clr/ui": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/@clr/ui/-/ui-3.0.1.tgz",
-      "integrity": "sha512-/g7ohS68dHFcz9FQSjy2MmcGQlKT7T/nK14sY+maSLUB8f1ht5fM4WJ/PGv2mPm7US4Ag8BXTQnWdXR6P/BMUw=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@clr/ui/-/ui-3.1.2.tgz",
+      "integrity": "sha512-loThtniaXFJZET0DN3JAqbJssh9vYtkCz+dcmIkEPG8ZGmjPDFoQ437/Avxu0FtEIB3HlKpDDjuoEd0+uXZKqQ=="
     },
     "@develar/schema-utils": {
       "version": "2.1.0",
@@ -5199,9 +5199,9 @@
       }
     },
     "css-vars-ponyfill": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/css-vars-ponyfill/-/css-vars-ponyfill-2.2.1.tgz",
-      "integrity": "sha512-bFdKEQbCtwoDl4/iPaqjGDMZRgnVn6k7c6XUlOrW+OzMIweWV6b5YbajR99dUiAsB1WpfbGGiiE9T1cnSh2pgA=="
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/css-vars-ponyfill/-/css-vars-ponyfill-2.3.1.tgz",
+      "integrity": "sha512-LHLFhKKQChkQz6nItHU1ZA/ABYJbw489eF0APx2a2FvjT32fAnbqriTLzKsl4KXowuVXcwoRWh8iQNO7wNzuGA=="
     },
     "css-what": {
       "version": "3.2.1",
diff --git a/web/package.json b/web/package.json
index e035f5cb..f1d2391d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -34,10 +34,10 @@
     "@angular/platform-browser": "^9.1.0",
     "@angular/platform-browser-dynamic": "^9.1.0",
     "@angular/router": "^9.1.0",
-    "@clr/angular": "^3.0.1",
-    "@clr/core": "^3.0.1",
-    "@clr/icons": "^3.0.1",
-    "@clr/ui": "^3.0.1",
+    "@clr/angular": "^3.1.2",
+    "@clr/core": "^3.1.2",
+    "@clr/icons": "^3.1.2",
+    "@clr/ui": "^3.1.2",
     "@ng-select/ng-select": "^3.7.3",
     "@types/cytoscape": "^3.14.0",
     "@webcomponents/custom-elements": "^1.4.1",
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 85822bf2..ac5e409c 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
@@ -17,8 +17,11 @@
       ></app-content-filter>
     </clr-dg-filter>
   </clr-dg-column>
-  <clr-dg-row *clrDgItems="let row of rowsWithMetadata; trackBy: identifyRow">
-    <clr-dg-action-overflow *ngIf="row.actions.length > 0">
+  <clr-dg-row
+    *clrDgItems="let row of rowsWithMetadata; trackBy: identifyRow"
+    [ngClass]="rowClass(row)"
+  >
+    <clr-dg-action-overflow *ngIf="row.actions.length > 0 && !row.isDeleted">
       <ng-container *ngFor="let action of row.actions">
         <button class="action-item" (click)="runAction(action)">
           {{ action.name }}
@@ -29,7 +32,6 @@
       <app-content-switcher [view]="row.data[column]"></app-content-switcher>
     </clr-dg-cell>
   </clr-dg-row>
-
   <clr-dg-footer>
     <clr-dg-pagination #pagination [clrDgPageSize]="10">
       <clr-dg-page-size [clrPageSizeOptions]="[10, 20, 50, 100]">
diff --git a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
index e78526e0..f3b6890f 100644
--- a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
+++ b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
@@ -1,3 +1,16 @@
 /* Copyright (c) 2019 the Octant contributors. All Rights Reserved.
  * SPDX-License-Identifier: Apache-2.0
  */
+
+.row-deleted {
+  :host-context(body.dark) {
+    --barberpole-color: hsl(9, 95%, 12%)
+  }
+
+  :host-context(body) {
+    --barberpole-color: hsl(9, 83%, 76%)
+  }
+
+  background-color: var(--barberpole-color);
+}
+
diff --git a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.ts b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.ts
index 2627e414..f4ae5594 100644
--- a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.ts
+++ b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.ts
@@ -79,16 +79,20 @@ export class DatagridComponent implements OnChanges {
     }
   }
 
-  private getRowsWithMetadata(rows: TableRow[]) {
+  private getRowsWithMetadata(rows: TableRow[]): TableRowWithMetadata[] {
     return rows.map(row => {
       let actions: GridAction[] = [];
 
       if (row.hasOwnProperty('_action')) {
         actions = (row._action as GridActionsView).config.actions;
       }
+
+      const isDeleted = !!row._isDeleted;
+
       return {
         data: row,
         actions,
+        isDeleted,
       };
     });
   }
@@ -117,6 +121,10 @@ export class DatagridComponent implements OnChanges {
     return true;
   }
 
+  rowClass(row: TableRowWithMetadata) {
+    return row.isDeleted ? ['row-deleted'] : [];
+  }
+
   cancelModal() {
     this.resetModal();
   }
@@ -143,6 +151,7 @@ export class DatagridComponent implements OnChanges {
 interface TableRowWithMetadata {
   data: TableRow;
   actions?: GridAction[];
+  isDeleted: boolean;
 }
 
 interface ActionDialogOptions {
diff --git a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
index 6ff0cf34..ce4a7158 100644
--- a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
+++ b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.html
@@ -1,3 +1,14 @@
-<ng-container *ngIf="name(); let name">
-  <div class="indicator" [className]="name"></div>
+<ng-container *ngIf="currentStatus">
+  <ng-container *ngIf="detail !== undefined; else showStatus">
+    <clr-signpost>
+      <div [ngClass]="[currentStatus, 'indicator']" clrSignpostTrigger></div>
+      <clr-signpost-content *clrIfOpen>
+        <app-content-switcher [view]="detail"></app-content-switcher>
+      </clr-signpost-content>
+    </clr-signpost>
+  </ng-container>
 </ng-container>
+
+<ng-template #showStatus>
+  <div [ngClass]="[currentStatus, 'indicator']" clrSignpostTrigger></div>
+</ng-template>
diff --git a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.scss b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.scss
index dee88cb4..5066dc18 100644
--- a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.scss
+++ b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.scss
@@ -6,21 +6,34 @@
 $circumference: 4px;
 
 .indicator {
+  :host-context(body.dark) {
+    --ok-background-color: #60b515;
+    --warning-background-color: #f57600;
+    --error-background-color: #e12200;
+  }
+
+  :host-context(body) {
+    --ok-background-color: #60b515;
+    --warning-background-color: #f57600;
+    --error-background-color: #e12200;
+  }
+
   height: $circumference;
   width: $circumference;
   border-radius: 50%;
   display: inline-block;
   transform: translateY(-50%);
+  margin-right: 0.5em;
 
   &.ok {
-    background-color: #60b515;
+    background-color: var(--ok-background-color);
   }
 
   &.warning {
-    background-color: #f57600;
+    background-color: var(--warning-background-color);
   }
 
   &.error {
-    background-color: #e12200;
+    background-color: var(--error-background-color);
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.spec.ts b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.spec.ts
index c6b164fd..6cdcdd9b 100644
--- a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.spec.ts
@@ -1,17 +1,19 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
 import {
   IndicatorComponent,
   Status,
   statusLookup,
 } from './indicator.component';
 import { Component } from '@angular/core';
+import { View } from '../../../models/content';
 
 @Component({
-  template: '<app-indicator [status]="status"></app-indicator>',
+  template:
+    '<app-indicator [status]="status" [detail]="detail"></app-indicator>',
 })
 class WrapperComponent {
   status: number;
+  detail: View;
 }
 
 describe('IndicatorComponent', () => {
@@ -39,6 +41,11 @@ describe('IndicatorComponent', () => {
       beforeEach(() => {
         element = fixture.nativeElement;
         component.status = v;
+        component.detail = {
+          metadata: {
+            type: 'test',
+          },
+        };
         fixture.detectChanges();
       });
 
diff --git a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.ts b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.ts
index 76bd6b99..59b0b79d 100644
--- a/web/src/app/modules/shared/components/presentation/indicator/indicator.component.ts
+++ b/web/src/app/modules/shared/components/presentation/indicator/indicator.component.ts
@@ -1,4 +1,5 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { View } from '../../../models/content';
 
 /**
  * Status are statuses known to indicator.
@@ -20,15 +21,24 @@ export const statusLookup = {
   templateUrl: './indicator.component.html',
   styleUrls: ['./indicator.component.scss'],
 })
-export class IndicatorComponent implements OnInit {
+export class IndicatorComponent implements OnChanges {
   @Input()
   status: number;
 
+  @Input()
+  detail: View;
+
+  currentStatus: string;
+
   constructor() {}
 
-  ngOnInit(): void {}
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes.status) {
+      this.currentStatus = statusLookup[changes.status.currentValue];
+    }
+  }
 
-  name() {
+  name(): string {
     return statusLookup[this.status];
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/link/link.component.html b/web/src/app/modules/shared/components/presentation/link/link.component.html
index f23712d7..125d6662 100644
--- a/web/src/app/modules/shared/components/presentation/link/link.component.html
+++ b/web/src/app/modules/shared/components/presentation/link/link.component.html
@@ -1,7 +1,16 @@
 <ng-template [ngIf]="isAbsolute" [ngIfElse]="relative">
+  <ng-container *ngIf="hasStatus">
+    <app-indicator [status]="view.config.status"></app-indicator>
+  </ng-container>
   <a [routerLink]="ref">{{ value }}</a>
 </ng-template>
 
 <ng-template #relative>
+  <ng-container *ngIf="hasStatus">
+    <app-indicator
+      [status]="view.config.status"
+      [detail]="view.config.statusDetail"
+    ></app-indicator>
+  </ng-container>
   <a [routerLink]="[ref]">{{ value }}</a>
 </ng-template>
diff --git a/web/src/app/modules/shared/components/presentation/link/link.component.ts b/web/src/app/modules/shared/components/presentation/link/link.component.ts
index 25ff2011..503d3ff6 100644
--- a/web/src/app/modules/shared/components/presentation/link/link.component.ts
+++ b/web/src/app/modules/shared/components/presentation/link/link.component.ts
@@ -25,6 +25,7 @@ export class LinkComponent implements OnChanges {
   ref: string;
   value: string;
   isAbsolute: boolean;
+  hasStatus: boolean;
 
   constructor() {}
 
@@ -34,6 +35,10 @@ export class LinkComponent implements OnChanges {
       this.ref = view.config.ref;
       this.value = view.config.value;
       this.isAbsolute = isUrlAbsolute(this.ref);
+
+      if (view.config.status) {
+        this.hasStatus = true;
+      }
     }
   }
 }
diff --git a/web/src/app/modules/shared/components/presentation/text/text.component.html b/web/src/app/modules/shared/components/presentation/text/text.component.html
index 8778bbfa..e7000955 100644
--- a/web/src/app/modules/shared/components/presentation/text/text.component.html
+++ b/web/src/app/modules/shared/components/presentation/text/text.component.html
@@ -1,6 +1,6 @@
 <ng-container *ngIf="!isMarkdown; else markdown">
   <ng-container *ngIf="hasStatus">
-    <app-indicator [status]="v.config.status"></app-indicator>
+    <app-indicator [status]="view.config.status"></app-indicator>
   </ng-container>
   {{ value }}
 </ng-container>
diff --git a/web/src/app/modules/shared/components/presentation/text/text.component.spec.ts b/web/src/app/modules/shared/components/presentation/text/text.component.spec.ts
index da1de1e0..1e8ac341 100644
--- a/web/src/app/modules/shared/components/presentation/text/text.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/text/text.component.spec.ts
@@ -6,7 +6,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { TextView } from '../../../models/content';
 import { TextComponent } from './text.component';
-import { SharedModule } from '../../../shared.module';
 import { Status } from '../indicator/indicator.component';
 
 @Component({
@@ -23,8 +22,7 @@ describe('TextComponent', () => {
 
     beforeEach(async(() => {
       TestBed.configureTestingModule({
-        declarations: [TestWrapperComponent],
-        imports: [SharedModule],
+        declarations: [TestWrapperComponent, TextComponent],
       }).compileComponents();
     }));
 
diff --git a/web/src/app/modules/shared/components/presentation/text/text.component.ts b/web/src/app/modules/shared/components/presentation/text/text.component.ts
index 4ee4ff0d..b18ef443 100644
--- a/web/src/app/modules/shared/components/presentation/text/text.component.ts
+++ b/web/src/app/modules/shared/components/presentation/text/text.component.ts
@@ -12,11 +12,11 @@ import { TextView, View } from 'src/app/modules/shared/models/content';
 export class TextComponent implements OnChanges {
   private v: TextView;
 
-  @Input() set view(v: View) {
+  @Input() set view(v: TextView) {
     this.v = v as TextView;
   }
 
-  get view() {
+  get view(): TextView {
     return this.v;
   }
 
diff --git a/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.html b/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.html
index aee59e89..fa6bfae3 100644
--- a/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.html
+++ b/web/src/app/modules/shared/components/presentation/timestamp/timestamp.component.html
@@ -1,6 +1,6 @@
 <span class="tooltip tooltip-wrapper tooltip-md">
   <span>{{ timestamp | relative }}</span>
-     <span class="tooltip-content" [ngStyle]="{ 'margin-top': getScrollPos() }">
-       <span>{{ humanReadable }}</span>
-     </span>
+  <span class="tooltip-content" [ngStyle]="{ 'margin-top': getScrollPos() }">
+    <span>{{ humanReadable }}</span>
+  </span>
 </span>
diff --git a/web/src/app/modules/shared/components/smart/terminal/terminal.component.html b/web/src/app/modules/shared/components/smart/terminal/terminal.component.html
index aa227540..8e3fd0d7 100644
--- a/web/src/app/modules/shared/components/smart/terminal/terminal.component.html
+++ b/web/src/app/modules/shared/components/smart/terminal/terminal.component.html
@@ -1,7 +1,19 @@
 <clr-select-container class="container-select">
   <label>Container</label>
-  <select clrSelect name="options" [value]="selectedContainer" (change)="onContainerChange($event.target.value)">
-    <option *ngFor="let container of view?.config.containers; trackBy: trackByIdentity" value="{{ container }}">{{ container }}</option>
+  <select
+    clrSelect
+    name="options"
+    [value]="selectedContainer"
+    (change)="onContainerChange($event.target.value)"
+  >
+    <option
+      *ngFor="
+        let container of view?.config.containers;
+        trackBy: trackByIdentity
+      "
+      value="{{ container }}"
+      >{{ container }}</option
+    >
   </select>
 </clr-select-container>
 <div class="app-terminal" #terminal></div>
diff --git a/web/src/app/modules/shared/models/content.ts b/web/src/app/modules/shared/models/content.ts
index 0d3dbe22..606fbcdb 100644
--- a/web/src/app/modules/shared/models/content.ts
+++ b/web/src/app/modules/shared/models/content.ts
@@ -150,6 +150,8 @@ export interface LinkView extends View {
   config: {
     ref: string;
     value: string;
+    status?: number;
+    statusDetail?: View;
   };
 }
 
diff --git a/web/src/index.html b/web/src/index.html
index 1ddb93cf..a94b1a67 100644
--- a/web/src/index.html
+++ b/web/src/index.html
@@ -1,17 +1,17 @@
-<!doctype html>
+<!DOCTYPE html>
 <html lang="en">
-<head>
-  <meta charset="utf-8">
-  <title>Octant</title>
-  <base href="/">
+  <head>
+    <meta charset="utf-8" />
+    <title>Octant</title>
+    <base href="/" />
 
-  <!-- Older IE clients need no-cache -->
-  <meta http-equiv="cache-control" content="no-cache" />
-  <meta http-equiv="cache-control" content="no-store" />
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="icon" type="image/x-icon" href="favicon.ico">
-</head>
-<body>
-  <app-home></app-home>
-</body>
+    <!-- Older IE clients need no-cache -->
+    <meta http-equiv="cache-control" content="no-cache" />
+    <meta http-equiv="cache-control" content="no-store" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <link rel="icon" type="image/x-icon" href="favicon.ico" />
+  </head>
+  <body>
+    <app-home></app-home>
+  </body>
 </html>


Test patch:
diff --git a/internal/printer/clusterrole_test.go b/internal/printer/clusterrole_test.go
index 41acac5e..61b3e9de 100644
--- a/internal/printer/clusterrole_test.go
+++ b/internal/printer/clusterrole_test.go
@@ -44,8 +44,11 @@ func Test_ClusterRoleListHandler(t *testing.T) {
 	expected := component.NewTable("Cluster Roles", "We couldn't find any cluster roles!", cols)
 
 	expected.Add(component.TableRow{
-		"Name": component.NewLink("", object.Name, "/path"),
-		"Age":  component.NewTimestamp(now),
+		"Name": component.NewLink("", object.Name, "/path",
+			genObjectStatus(component.TextStatusOK, []string{
+				"rbac.authorization.k8s.io/v1 ClusterRole is OK",
+			})),
+		"Age": component.NewTimestamp(now),
 		component.GridActionKey: gridActionsFactory([]component.GridAction{
 			buildObjectDeleteAction(t, object),
 		}),
@@ -80,23 +83,25 @@ func Test_ClusterRoleConfiguration(t *testing.T) {
 		},
 	}
 
-	for _, tc := range cases {
-		controller := gomock.NewController(t)
-		defer controller.Finish()
+	for _, test := range cases {
+		t.Run(test.name, func(t *testing.T) {
+			controller := gomock.NewController(t)
+			defer controller.Finish()
 
-		tpo := newTestPrinterOptions(controller)
-		printOptions := tpo.ToOptions()
+			tpo := newTestPrinterOptions(controller)
+			printOptions := tpo.ToOptions()
 
-		cc := NewClusterRoleConfiguration(tc.clusterRole)
+			cc := NewClusterRoleConfiguration(test.clusterRole)
 
-		summary, err := cc.Create(printOptions)
-		if tc.isErr {
-			require.Error(t, err)
-			return
-		}
-		require.NoError(t, err)
+			summary, err := cc.Create(printOptions)
+			if test.isErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
 
-		component.AssertEqual(t, tc.expected, summary)
+			component.AssertEqual(t, test.expected, summary)
+		})
 	}
 }
 
diff --git a/internal/printer/clusterrolebinding_test.go b/internal/printer/clusterrolebinding_test.go
index bd7fc77c..e1e794a4 100644
--- a/internal/printer/clusterrolebinding_test.go
+++ b/internal/printer/clusterrolebinding_test.go
@@ -53,7 +53,10 @@ func Test_ClusterRoleBindingListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Age", "Role kind", "Role name")
 	expected := component.NewTable("Cluster Role Bindings", "We couldn't find any cluster role bindings!", cols)
 	expected.Add(component.TableRow{
-		"Name":      component.NewLink("", clusterRoleBinding.Name, "/cluster-role-binding-path"),
+		"Name": component.NewLink("", clusterRoleBinding.Name, "/cluster-role-binding-path",
+			genObjectStatus(component.TextStatusOK, []string{
+				"rbac.authorization.k8s.io/v1 ClusterRoleBinding is OK",
+			})),
 		"Labels":    component.NewLabels(labels),
 		"Age":       component.NewTimestamp(now),
 		"Role kind": component.NewText("Role"),
diff --git a/internal/printer/configmap_test.go b/internal/printer/configmap_test.go
index 19df89b3..0376f751 100644
--- a/internal/printer/configmap_test.go
+++ b/internal/printer/configmap_test.go
@@ -52,7 +52,8 @@ func Test_ConfigMapListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Data", "Age")
 	expected := component.NewTable("ConfigMaps", "We couldn't find any config maps!", cols)
 	expected.Add(component.TableRow{
-		"Name":   component.NewLink("", configMap.Name, "/configMap"),
+		"Name": component.NewLink("", configMap.Name, "/configMap",
+			genObjectStatus(component.TextStatusOK, []string{"v1 ConfigMap is OK"})),
 		"Labels": component.NewLabels(labels),
 		"Data":   component.NewText("2"),
 		"Age":    component.NewTimestamp(now),
diff --git a/internal/printer/cronjob_test.go b/internal/printer/cronjob_test.go
index 09199168..e8f2c62a 100644
--- a/internal/printer/cronjob_test.go
+++ b/internal/printer/cronjob_test.go
@@ -83,7 +83,13 @@ func Test_CronJobListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Schedule", "Age", "")
 	expected := component.NewTable("CronJobs", "We couldn't find any cron jobs!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "cron", "/cron"),
+		"Name": component.NewLink("", "cron", "/cron", func(l *component.Link) {
+			l.SetStatus(component.TextStatusOK,
+				component.NewList(nil, []component.Component{
+					component.NewText("batch/v1beta1 CronJob is OK"),
+				},
+				))
+		}),
 		"Labels":   component.NewLabels(labels),
 		"Schedule": component.NewText("*/1 * * * *"),
 		"Age":      component.NewTimestamp(now),
@@ -236,7 +242,12 @@ func Test_createJobListView(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Completions", "Successful", "Age")
 	expected := component.NewTable("Jobs", "We couldn't find any jobs!", cols)
 	expected.Add(component.TableRow{
-		"Name":        component.NewLink("", "job", "/job"),
+		"Name": component.NewLink("", "job", "/job",
+			genObjectStatus(component.TextStatusWarning, []string{
+				"Job has succeeded 1 time",
+				"Job is in progress",
+			}),
+		),
 		"Labels":      component.NewLabels(labels),
 		"Completions": component.NewText("1"),
 		"Successful":  component.NewText("1"),
diff --git a/internal/printer/daemonset_test.go b/internal/printer/daemonset_test.go
index bcfc0fa5..1408d811 100644
--- a/internal/printer/daemonset_test.go
+++ b/internal/printer/daemonset_test.go
@@ -53,7 +53,8 @@ func Test_DaemonSetListHandler(t *testing.T) {
 		"Up-To-Date", "Age", "Node Selector")
 	expected := component.NewTable("Daemon Sets", "We couldn't find any daemon sets!", cols)
 	expected.Add(component.TableRow{
-		"Name":          component.NewLink("", object.Name, "/path"),
+		"Name": component.NewLink("", object.Name, "/path",
+			genObjectStatus(component.TextStatusOK, []string{"Daemon Set is OK"})),
 		"Labels":        component.NewLabels(labels),
 		"Age":           component.NewTimestamp(now),
 		"Desired":       component.NewText("1"),
@@ -220,7 +221,8 @@ func Test_DaemonSetPods(t *testing.T) {
 	cols := component.NewTableCols("Name", "Ready", "Phase", "Restarts", "Node", "Age")
 	expected := component.NewTable("Pods", "We couldn't find any pods!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "fluentd-elasticsearch-dvskv", "/pod"),
+		"Name": component.NewLink("", "fluentd-elasticsearch-dvskv", "/pod",
+			genObjectStatus(component.TextStatusWarning, []string{""})),
 		"Ready":    component.NewText("0/1"),
 		"Phase":    component.NewText("Pending"),
 		"Restarts": component.NewText("0"),
diff --git a/internal/printer/deployment_test.go b/internal/printer/deployment_test.go
index ae60caf5..e57a0604 100644
--- a/internal/printer/deployment_test.go
+++ b/internal/printer/deployment_test.go
@@ -100,7 +100,9 @@ func Test_DeploymentListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Status", "Age", "Containers", "Selector")
 	expected := component.NewTable("Deployments", "We couldn't find any deployments!", cols)
 	expected.Add(component.TableRow{
-		"Name":       component.NewLink("", "deployment", "/path"),
+		"Name": component.NewLink("", "deployment", "/path",
+			genObjectStatus(component.TextStatusWarning, []string{
+				"Expected 3 replicas, but 2 are available"})),
 		"Labels":     component.NewLabels(objectLabels),
 		"Age":        component.NewTimestamp(now),
 		"Selector":   component.NewSelectors([]component.Selector{component.NewLabelSelector("app", "my_app")}),
@@ -341,7 +343,8 @@ func Test_DeploymentPods(t *testing.T) {
 
 	expected := component.NewTableWithRows("Pods", "We couldn't find any pods!", podColsWithOutLabels, []component.TableRow{
 		{
-			"Name":     component.NewLink("", pod.Name, "/pod"),
+			"Name": component.NewLink("", pod.Name, "/pod",
+				genObjectStatus(component.TextStatusOK, []string{""})),
 			"Age":      component.NewTimestamp(now),
 			"Ready":    component.NewText("1/1"),
 			"Restarts": component.NewText("0"),
diff --git a/internal/printer/helpers_test.go b/internal/printer/helpers_test.go
index 938ab3cc..7d039452 100644
--- a/internal/printer/helpers_test.go
+++ b/internal/printer/helpers_test.go
@@ -94,3 +94,18 @@ func buildObjectDeleteAction(t *testing.T, object runtime.Object) component.Grid
 
 	return action
 }
+
+// genObjectStatus generates object status for a link. It can be used
+// when testing list handlers. This will be needed until there is a
+// way to test that the list handlers are working without external
+// dependencies.
+func genObjectStatus(status component.TextStatus, messages []string) component.LinkOption {
+	list := component.NewList(nil, nil)
+	for _, msg := range messages {
+		list.Add(component.NewText(msg))
+	}
+
+	return func(l *component.Link) {
+		l.SetStatus(status, list)
+	}
+}
diff --git a/internal/printer/horizontalpodautoscaler_test.go b/internal/printer/horizontalpodautoscaler_test.go
index b913d663..fb63551d 100644
--- a/internal/printer/horizontalpodautoscaler_test.go
+++ b/internal/printer/horizontalpodautoscaler_test.go
@@ -75,7 +75,8 @@ func Test_HorizontalPodAutoscalerListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Targets", "Minimum Pods", "Maximum Pods", "Replicas", "Age")
 	expected := component.NewTable("Horizontal Pod Autoscalers", "We couldn't find any horizontal pod autoscalers", cols)
 	expected.Add(component.TableRow{
-		"Name":         component.NewLink("", "horizontalpodautoscaler", "/path"),
+		"Name": component.NewLink("", "horizontalpodautoscaler", "/path",
+			genObjectStatus(component.TextStatusOK, []string{"autoscaling/v1 HorizontalPodAutoscaler is OK"})),
 		"Labels":       component.NewLabels(objectLabels),
 		"Targets":      component.NewText("5/50%"),
 		"Minimum Pods": component.NewText("1"),
diff --git a/internal/printer/ingress_test.go b/internal/printer/ingress_test.go
index 0ebc613b..fa1a5e59 100644
--- a/internal/printer/ingress_test.go
+++ b/internal/printer/ingress_test.go
@@ -17,6 +17,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/vmware-tanzu/octant/internal/testutil"
+	"github.com/vmware-tanzu/octant/pkg/store"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
@@ -46,6 +47,8 @@ func Test_IngressListHandler(t *testing.T) {
 
 	cols := component.NewTableCols("Name", "Labels", "Hosts", "Address", "Ports", "Age")
 
+	service := testutil.ToUnstructured(t, testutil.CreateService("service"))
+
 	cases := []struct {
 		name     string
 		list     *extv1beta1.IngressList
@@ -58,7 +61,10 @@ func Test_IngressListHandler(t *testing.T) {
 			expected: component.NewTableWithRows("Ingresses", "We couldn't find any ingresses!", cols,
 				[]component.TableRow{
 					{
-						"Name":    component.NewLink("", "ingress", "/ingress"),
+						"Name": component.NewLink("", "ingress", "/ingress",
+							genObjectStatus(component.TextStatusError, []string{
+								`Backend for service "app" specifies an invalid port`,
+							})),
 						"Labels":  component.NewLabels(labels),
 						"Age":     component.NewTimestamp(now),
 						"Hosts":   component.NewText("*"),
@@ -76,7 +82,11 @@ func Test_IngressListHandler(t *testing.T) {
 			expected: component.NewTableWithRows("Ingresses", "We couldn't find any ingresses!", cols,
 				[]component.TableRow{
 					{
-						"Name":    component.NewLink("", "ingress", "/ingress"),
+						"Name": component.NewLink("", "ingress", "/ingress",
+							genObjectStatus(component.TextStatusError, []string{
+								`Backend for service "app" specifies an invalid port`,
+								"TLS configuration did not define a secret name",
+							})),
 						"Labels":  component.NewLabels(labels),
 						"Age":     component.NewTimestamp(now),
 						"Hosts":   component.NewText("*"),
@@ -108,6 +118,15 @@ func Test_IngressListHandler(t *testing.T) {
 
 			}
 
+			tpo.objectStore.EXPECT().
+				Get(gomock.Any(), store.Key{
+					Namespace:  "namespace",
+					APIVersion: "v1",
+					Kind:       "Service",
+					Name:       "app"}).
+				Return(service, nil).
+				AnyTimes()
+
 			ctx := context.Background()
 			got, err := IngressListHandler(ctx, tc.list, printOptions)
 			if tc.isErr {
diff --git a/internal/printer/job_test.go b/internal/printer/job_test.go
index 303123c1..7500785e 100644
--- a/internal/printer/job_test.go
+++ b/internal/printer/job_test.go
@@ -64,7 +64,11 @@ func Test_JobListHandler(t *testing.T) {
 
 	expected := component.NewTable("Jobs", "We couldn't find any jobs!", JobCols)
 	expected.Add(component.TableRow{
-		"Name":        component.NewLink("", "job", "/job"),
+		"Name": component.NewLink("", "job", "/job",
+			genObjectStatus(component.TextStatusWarning, []string{
+				"Job has succeeded 1 time",
+				"Job is in progress",
+			})),
 		"Labels":      component.NewLabels(validJobLabels),
 		"Completions": component.NewText("1"),
 		"Successful":  component.NewText("1"),
diff --git a/internal/printer/networkpolicy_test.go b/internal/printer/networkpolicy_test.go
index 41058169..1e35ba40 100644
--- a/internal/printer/networkpolicy_test.go
+++ b/internal/printer/networkpolicy_test.go
@@ -46,7 +46,8 @@ func Test_NetworkPolicyListHandler(t *testing.T) {
 			expected: component.NewTableWithRows("Network Policies", "We couldn't find any network policies!", cols,
 				[]component.TableRow{
 					{
-						"Name":   component.NewLink("", "networkPolicy", "/networkPolicy"),
+						"Name": component.NewLink("", "networkPolicy", "/networkPolicy",
+							genObjectStatus(component.TextStatusOK, []string{"v1 Namespace is OK"})),
 						"Labels": component.NewLabels(labels),
 						"Age":    component.NewTimestamp(now),
 						component.GridActionKey: gridActionsFactory([]component.GridAction{
diff --git a/internal/printer/object_table_test.go b/internal/printer/object_table_test.go
index 1389d0e4..47c235ee 100644
--- a/internal/printer/object_table_test.go
+++ b/internal/printer/object_table_test.go
@@ -6,14 +6,17 @@
 package printer
 
 import (
+	"context"
 	"fmt"
 	"testing"
 
+	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/vmware-tanzu/octant/internal/testutil"
+	"github.com/vmware-tanzu/octant/pkg/store/fake"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
@@ -27,7 +30,19 @@ func TestObjectTable(t *testing.T) {
 	}
 
 	pod1 := testutil.CreatePod("pod1")
+	pod1A := component.NewLink("", "pod1", "/pod1", func(l *component.Link) {
+		list := component.NewList(nil, []component.Component{
+			component.NewText(""),
+		})
+		l.SetStatus(component.TextStatusWarning, list)
+	})
 	pod2 := testutil.CreatePod("pod2")
+	pod2A := component.NewLink("", "pod2", "/pod2", func(l *component.Link) {
+		list := component.NewList(nil, []component.Component{
+			component.NewText(""),
+		})
+		l.SetStatus(component.TextStatusWarning, list)
+	})
 
 	tests := []struct {
 		name     string
@@ -40,14 +55,15 @@ func TestObjectTable(t *testing.T) {
 
 			},
 			wanted: func() *component.Table {
+
 				return component.NewTableWithRows("table", "placeholder", cols, []component.TableRow{
 					{
-						"A":                     component.NewText("pod1"),
+						"A":                     pod1A,
 						"B":                     component.NewText("0"),
 						component.GridActionKey: genDeleteGA(pod1),
 					},
 					{
-						"A":                     component.NewText("pod2"),
+						"A":                     pod2A,
 						"B":                     component.NewText("1"),
 						component.GridActionKey: genDeleteGA(pod2),
 					},
@@ -62,12 +78,12 @@ func TestObjectTable(t *testing.T) {
 			wanted: func() *component.Table {
 				return component.NewTableWithRows("table", "placeholder", cols, []component.TableRow{
 					{
-						"A":                     component.NewText("pod2"),
+						"A":                     pod2A,
 						"B":                     component.NewText("1"),
 						component.GridActionKey: genDeleteGA(pod2),
 					},
 					{
-						"A":                     component.NewText("pod1"),
+						"A":                     pod1A,
 						"B":                     component.NewText("0"),
 						component.GridActionKey: genDeleteGA(pod1),
 					},
@@ -87,12 +103,12 @@ func TestObjectTable(t *testing.T) {
 			wanted: func() *component.Table {
 				table := component.NewTableWithRows("table", "placeholder", cols, []component.TableRow{
 					{
-						"A":                     component.NewText("pod1"),
+						"A":                     pod1A,
 						"B":                     component.NewText("0"),
 						component.GridActionKey: genDeleteGA(pod1),
 					},
 					{
-						"A":                     component.NewText("pod2"),
+						"A":                     pod2A,
 						"B":                     component.NewText("1"),
 						component.GridActionKey: genDeleteGA(pod2),
 					},
@@ -109,11 +125,18 @@ func TestObjectTable(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			ot := NewObjectTable("table", "placeholder", cols)
+			ctx := context.Background()
+
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+
+			objectStore := fake.NewMockStore(ctrl)
+
+			ot := NewObjectTable("table", "placeholder", cols, objectStore)
 
 			for i, pod := range []*corev1.Pod{pod1, pod2} {
-				err := ot.AddRowForObject(pod, component.TableRow{
-					"A": component.NewText(pod.Name),
+				err := ot.AddRowForObject(ctx, pod, component.TableRow{
+					"A": component.NewLink("", pod.Name, "/"+pod.Name),
 					"B": component.NewText(fmt.Sprintf("%d", i)),
 				})
 				require.NoError(t, err)
diff --git a/internal/printer/persistentvolume_test.go b/internal/printer/persistentvolume_test.go
index dcf9ad3b..75731ada 100644
--- a/internal/printer/persistentvolume_test.go
+++ b/internal/printer/persistentvolume_test.go
@@ -55,7 +55,10 @@ func Test_PersistentVolumeListHandler(t *testing.T) {
 			expected: component.NewTableWithRows("Persistent Volumes", "We couldn't find any persistent volumes!", cols,
 				[]component.TableRow{
 					{
-						"Name":           component.NewLink("", "persistentVolume", "/persistentVolume"),
+						"Name": component.NewLink("", "persistentVolume", "/persistentVolume",
+							genObjectStatus(component.TextStatusOK, []string{
+								"v1 PersistentVolume is OK",
+							})),
 						"Capacity":       component.NewText("0"),
 						"Access Modes":   component.NewText(""),
 						"Reclaim Policy": component.NewText(""),
@@ -76,7 +79,10 @@ func Test_PersistentVolumeListHandler(t *testing.T) {
 			expected: component.NewTableWithRows("Persistent Volumes", "We couldn't find any persistent volumes!", cols,
 				[]component.TableRow{
 					{
-						"Name":           component.NewLink("", "unboundPersistentVolume", "/unboundPersistentVolume"),
+						"Name": component.NewLink("", "unboundPersistentVolume", "/unboundPersistentVolume",
+							genObjectStatus(component.TextStatusOK, []string{
+								"v1 PersistentVolume is OK",
+							})),
 						"Capacity":       component.NewText("0"),
 						"Access Modes":   component.NewText(""),
 						"Reclaim Policy": component.NewText(""),
diff --git a/internal/printer/persistentvolumeclaim_test.go b/internal/printer/persistentvolumeclaim_test.go
index 15d02ef9..f6982298 100644
--- a/internal/printer/persistentvolumeclaim_test.go
+++ b/internal/printer/persistentvolumeclaim_test.go
@@ -53,7 +53,10 @@ func Test_PersistentVolumeClaimListHandler(t *testing.T) {
 			name:             "bounded",
 			persistentvolume: pv,
 			expected: component.TableRow{
-				"Name":          component.NewLink("", object.Name, "/pvc"),
+				"Name": component.NewLink("", object.Name, "/pvc",
+					genObjectStatus(component.TextStatusOK, []string{
+						"v1 PersistentVolumeClaim is OK",
+					})),
 				"Status":        component.NewText("Bound"),
 				"Volume":        component.NewLink("", pv.GetName(), fmt.Sprintf("/%s", pv.GetName())),
 				"Capacity":      component.NewText("10Gi"),
@@ -68,7 +71,10 @@ func Test_PersistentVolumeClaimListHandler(t *testing.T) {
 		{
 			name: "unbounded",
 			expected: component.TableRow{
-				"Name":          component.NewLink("", object.Name, "/pvc"),
+				"Name": component.NewLink("", object.Name, "/pvc",
+					genObjectStatus(component.TextStatusOK, []string{
+						"v1 PersistentVolumeClaim is OK",
+					})),
 				"Status":        component.NewText("Bound"),
 				"Volume":        component.NewText(""),
 				"Capacity":      component.NewText(""),
@@ -313,7 +319,9 @@ func Test_PersistentVolumeClaimMountedPodsList(t *testing.T) {
 	cols := component.NewTableCols("Name", "Ready", "Phase", "Restarts", "Node", "Age")
 	expected := component.NewTable("Pods", "We couldn't find any pods!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "wordpress-mysql-67565bd57-8fzbh", "/pod"),
+		"Name": component.NewLink("", "wordpress-mysql-67565bd57-8fzbh", "/pod",
+			genObjectStatus(component.TextStatusOK, []string{""})),
+
 		"Ready":    component.NewText("1/1"),
 		"Phase":    component.NewText("Running"),
 		"Restarts": component.NewText("0"),
diff --git a/internal/printer/pod_test.go b/internal/printer/pod_test.go
index 443fb939..fcca5808 100644
--- a/internal/printer/pod_test.go
+++ b/internal/printer/pod_test.go
@@ -84,7 +84,10 @@ func Test_PodListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Ready", "Phase", "Restarts", "Node", "Age")
 	expected := component.NewTable("Pods", "We couldn't find any pods!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "pod", "/pod"),
+		"Name": component.NewLink("", "pod", "/pod",
+			genObjectStatus(component.TextStatusWarning, []string{
+				"",
+			})),
 		"Labels":   component.NewLabels(labels),
 		"Ready":    component.NewText("1/2"),
 		"Phase":    component.NewText("Pending"),
@@ -148,7 +151,8 @@ func Test_PodListHandlerNoLabel(t *testing.T) {
 	cols := component.NewTableCols("Name", "Ready", "Phase", "Restarts", "Node", "Age")
 	expected := component.NewTable("Pods", "We couldn't find any pods!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "pi-7xpxr", "/pi-7xpxr"),
+		"Name": component.NewLink("", "pi-7xpxr", "/pi-7xpxr",
+			genObjectStatus(component.TextStatusWarning, []string{""})),
 		"Ready":    component.NewText("0/1"),
 		"Phase":    component.NewText("Succeeded"),
 		"Restarts": component.NewText("0"),
@@ -190,7 +194,8 @@ func TestPodListHandler_sorted(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Ready", "Phase", "Restarts", "Node", "Age")
 	expected := component.NewTable("Pods", "We couldn't find any pods!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "pod1", "/pod1"),
+		"Name": component.NewLink("", "pod1", "/pod1",
+			genObjectStatus(component.TextStatusWarning, []string{""})),
 		"Labels":   component.NewLabels(make(map[string]string)),
 		"Ready":    component.NewText("0/0"),
 		"Phase":    component.NewText(""),
@@ -202,7 +207,8 @@ func TestPodListHandler_sorted(t *testing.T) {
 		}),
 	})
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "pod2", "/pod2"),
+		"Name": component.NewLink("", "pod2", "/pod2",
+			genObjectStatus(component.TextStatusWarning, []string{""})),
 		"Labels":   component.NewLabels(make(map[string]string)),
 		"Ready":    component.NewText("0/0"),
 		"Phase":    component.NewText(""),
diff --git a/internal/printer/replicaset_test.go b/internal/printer/replicaset_test.go
index 8ad4f59d..78d90aa0 100644
--- a/internal/printer/replicaset_test.go
+++ b/internal/printer/replicaset_test.go
@@ -93,7 +93,10 @@ func Test_ReplicaSetListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Status", "Age", "Containers", "Selector")
 	expected := component.NewTable("ReplicaSets", "We couldn't find any replica sets!", cols)
 	expected.Add(component.TableRow{
-		"Name":       component.NewLink("", "replicaset-test", "/replica-set"),
+		"Name": component.NewLink("", "replicaset-test", "/replica-set",
+			genObjectStatus(component.TextStatusWarning, []string{
+				"Expected 3 replicas, but 2 are available",
+			})),
 		"Labels":     component.NewLabels(labels),
 		"Age":        component.NewTimestamp(now),
 		"Selector":   component.NewSelectors([]component.Selector{component.NewLabelSelector("app", "myapp")}),
@@ -316,7 +319,8 @@ func Test_ReplicaSetPods(t *testing.T) {
 	cols := component.NewTableCols("Name", "Ready", "Phase", "Restarts", "Node", "Age")
 	expected := component.NewTable("Pods", "We couldn't find any pods!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "nginx-deployment-59478d9757-nfqbk", "/pod"),
+		"Name": component.NewLink("", "nginx-deployment-59478d9757-nfqbk", "/pod",
+			genObjectStatus(component.TextStatusWarning, []string{""})),
 		"Ready":    component.NewText("0/1"),
 		"Phase":    component.NewText("Pending"),
 		"Restarts": component.NewText("0"),
diff --git a/internal/printer/replicationcontroller_test.go b/internal/printer/replicationcontroller_test.go
index 4cd3cd12..e45d8de5 100644
--- a/internal/printer/replicationcontroller_test.go
+++ b/internal/printer/replicationcontroller_test.go
@@ -86,7 +86,10 @@ func Test_ReplicationControllerListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Status", "Age", "Containers", "Selector")
 	expected := component.NewTable("ReplicationControllers", "We couldn't find any replication controllers!", cols)
 	expected.Add(component.TableRow{
-		"Name":       component.NewLink("", "rc-test", "/rc"),
+		"Name": component.NewLink("", "rc-test", "/rc",
+			genObjectStatus(component.TextStatusWarning, []string{
+				"Replication Controller pods are not ready",
+			})),
 		"Labels":     component.NewLabels(validReplicationControllerLabels),
 		"Status":     component.NewText("0/3"),
 		"Age":        component.NewTimestamp(validReplicationControllerCreationTime),
@@ -289,7 +292,8 @@ func Test_ReplicationControllerPods(t *testing.T) {
 	cols := component.NewTableCols("Name", "Ready", "Phase", "Restarts", "Node", "Age")
 	expected := component.NewTable("Pods", "We couldn't find any pods!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "nginx-hv4qs", "/pod"),
+		"Name": component.NewLink("", "nginx-hv4qs", "/pod",
+			genObjectStatus(component.TextStatusWarning, []string{""})),
 		"Ready":    component.NewText("0/1"),
 		"Phase":    component.NewText("Pending"),
 		"Restarts": component.NewText("0"),
diff --git a/internal/printer/role_test.go b/internal/printer/role_test.go
index 5537827c..0f4cdcc2 100644
--- a/internal/printer/role_test.go
+++ b/internal/printer/role_test.go
@@ -44,8 +44,11 @@ func Test_RoleListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Age")
 	expected := component.NewTable("Roles", "We couldn't find any roles!", cols)
 	expected.Add(component.TableRow{
-		"Name": component.NewLink("", role.Name, "/role"),
-		"Age":  component.NewTimestamp(role.CreationTimestamp.Time),
+		"Name": component.NewLink("", role.Name, "/role",
+			genObjectStatus(component.TextStatusOK, []string{
+				"rbac.authorization.k8s.io/v1 Role is OK",
+			})),
+		"Age": component.NewTimestamp(role.CreationTimestamp.Time),
 		component.GridActionKey: gridActionsFactory([]component.GridAction{
 			buildObjectDeleteAction(t, role),
 		}),
diff --git a/internal/printer/rolebinding_test.go b/internal/printer/rolebinding_test.go
index 031dfb95..0d487133 100644
--- a/internal/printer/rolebinding_test.go
+++ b/internal/printer/rolebinding_test.go
@@ -46,7 +46,10 @@ func Test_RoleBindingListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Age", "Role kind", "Role name")
 	expected := component.NewTable("Role Bindings", "We couldn't find any role bindings!", cols)
 	expected.Add(component.TableRow{
-		"Name":      component.NewLink("", roleBinding.Name, "/role-binding"),
+		"Name": component.NewLink("", roleBinding.Name, "/role-binding",
+			genObjectStatus(component.TextStatusOK, []string{
+				"rbac.authorization.k8s.io/v1 RoleBinding is OK",
+			})),
 		"Age":       component.NewTimestamp(now),
 		"Role kind": component.NewText("Role"),
 		"Role name": component.NewLink("", "pod-reader", "/role"),
diff --git a/internal/printer/secret_test.go b/internal/printer/secret_test.go
index 75c9f99d..e2ebb147 100644
--- a/internal/printer/secret_test.go
+++ b/internal/printer/secret_test.go
@@ -62,7 +62,10 @@ func Test_SecretListHandler(t *testing.T) {
 
 	expected := component.NewTable("Secrets", "We couldn't find any secrets!", secretTableCols)
 	expected.Add(component.TableRow{
-		"Name":   component.NewLink("", "secret", "/secret"),
+		"Name": component.NewLink("", "secret", "/secret",
+			genObjectStatus(component.TextStatusOK, []string{
+				"v1 Secret is OK",
+			})),
 		"Labels": component.NewLabels(labels),
 		"Type":   component.NewText("Opaque"),
 		"Data":   component.NewText("1"),
diff --git a/internal/printer/service_test.go b/internal/printer/service_test.go
index 8e7261b1..33989044 100644
--- a/internal/printer/service_test.go
+++ b/internal/printer/service_test.go
@@ -30,6 +30,16 @@ func Test_ServiceListHandler(t *testing.T) {
 	tpo := newTestPrinterOptions(controller)
 	printOptions := tpo.ToOptions()
 
+	endpoints := &unstructured.Unstructured{}
+
+	tpo.objectStore.EXPECT().
+		Get(gomock.Any(), store.Key{
+			Namespace:  "default",
+			APIVersion: "v1",
+			Kind:       "Endpoints",
+			Name:       "service",
+		}).Return(endpoints, nil).AnyTimes()
+
 	labels := map[string]string{
 		"foo": "bar",
 	}
@@ -86,7 +96,10 @@ func Test_ServiceListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Type", "Cluster IP", "External IP", "Ports", "Age", "Selector")
 	expected := component.NewTable("Services", "We couldn't find any services!", cols)
 	expected.Add(component.TableRow{
-		"Name":        component.NewLink("", "service", "/service"),
+		"Name": component.NewLink("", "service", "/service",
+			genObjectStatus(component.TextStatusWarning, []string{
+				"Service has no endpoint addresses",
+			})),
 		"Labels":      component.NewLabels(labels),
 		"Type":        component.NewText("ClusterIP"),
 		"Cluster IP":  component.NewText("1.2.3.4"),
diff --git a/internal/printer/serviceaccount_test.go b/internal/printer/serviceaccount_test.go
index 02283c43..8e040e3d 100644
--- a/internal/printer/serviceaccount_test.go
+++ b/internal/printer/serviceaccount_test.go
@@ -54,7 +54,10 @@ func Test_ServiceAccountListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Secrets", "Age")
 	expected := component.NewTable("Service Accounts", "We couldn't find any service accounts!", cols)
 	expected.Add(component.TableRow{
-		"Name":    component.NewLink("", object.Name, "/path"),
+		"Name": component.NewLink("", object.Name, "/path",
+			genObjectStatus(component.TextStatusOK, []string{
+				"v1 ServiceAccount is OK",
+			})),
 		"Labels":  component.NewLabels(labels),
 		"Secrets": component.NewText("1"),
 		"Age":     component.NewTimestamp(now),
diff --git a/internal/printer/statefulset_test.go b/internal/printer/statefulset_test.go
index 96da5d3c..cd1e94a3 100644
--- a/internal/printer/statefulset_test.go
+++ b/internal/printer/statefulset_test.go
@@ -84,7 +84,10 @@ func Test_StatefulSetListHandler(t *testing.T) {
 	cols := component.NewTableCols("Name", "Labels", "Desired", "Current", "Age", "Selector")
 	expected := component.NewTable("StatefulSets", "We couldn't find any stateful sets!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "web", "/path"),
+		"Name": component.NewLink("", "web", "/path",
+			genObjectStatus(component.TextStatusWarning, []string{
+				"Stateful Set pods are not ready",
+			})),
 		"Labels":   component.NewLabels(labels),
 		"Desired":  component.NewText("3"),
 		"Current":  component.NewText("1"),
@@ -338,7 +341,8 @@ func Test_StatefulSetPods(t *testing.T) {
 	cols := component.NewTableCols("Name", "Ready", "Phase", "Restarts", "Node", "Age")
 	expected := component.NewTable("Pods", "We couldn't find any pods!", cols)
 	expected.Add(component.TableRow{
-		"Name":     component.NewLink("", "web-0", "/pod"),
+		"Name": component.NewLink("", "web-0", "/pod",
+			genObjectStatus(component.TextStatusWarning, []string{""})),
 		"Ready":    component.NewText("1/1"),
 		"Phase":    component.NewText("Pending"),
 		"Restarts": component.NewText("0"),
diff --git a/pkg/dash/watcher_test.go b/pkg/dash/watcher_test.go
index e9c99591..eaa23205 100644
--- a/pkg/dash/watcher_test.go
+++ b/pkg/dash/watcher_test.go
@@ -71,7 +71,13 @@ func TestConfigWatcher_Watch(t *testing.T) {
 	watcherConfig := fake.NewMockWatcherConfig(controller)
 
 	watcherConfig.EXPECT().ContextName().Return("name")
-	watcherConfig.EXPECT().UseContext(gomock.Any(), "name").Return(nil)
+
+	ch := make(chan bool, 1)
+	watcherConfig.EXPECT().UseContext(gomock.Any(), "name").
+		DoAndReturn(func(_ context.Context, _ string) error {
+			ch <- true
+			return nil
+		})
 
 	fileWatcher := fake.NewMockFileWatcher(controller)
 
@@ -95,5 +101,6 @@ func TestConfigWatcher_Watch(t *testing.T) {
 	}
 
 	eventCh <- event
+	<-ch
 	cancel()
 }