Repository: vmware-tanzu/octant
Pull request #2507 (2021-05-31 05:15:29)

Bump @types/node from 14.14.43 to 14.17.1 in /web
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.14.43 to 14.17.1.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/node&package-manager=npm_and_yarn&previous-version=14.14.43&new-version=14.17.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/changelogs/unreleased/2409-mklanjsek b/changelogs/unreleased/2409-mklanjsek
new file mode 100644
index 00000000..41976c10
--- /dev/null
+++ b/changelogs/unreleased/2409-mklanjsek
@@ -0,0 +1,1 @@
+Added properties pane to Resource Viewer
diff --git a/cmd/octant-sample-plugin/main.go b/cmd/octant-sample-plugin/main.go
index a0ed91ff..b1f291b8 100644
--- a/cmd/octant-sample-plugin/main.go
+++ b/cmd/octant-sample-plugin/main.go
@@ -37,6 +37,7 @@ func main() {
 	// Tell Octant to call this plugin when printing configuration or tabs for Pods
 	capabilities := &plugin.Capabilities{
 		SupportsPrinterConfig: []schema.GroupVersionKind{podGVK},
+		SupportsObjectStatus:  []schema.GroupVersionKind{podGVK},
 		SupportsTab:           []schema.GroupVersionKind{podGVK},
 		ActionNames:           []string{pluginActionName},
 		IsModule:              true,
@@ -45,6 +46,7 @@ func main() {
 	// Set up what should happen when Octant calls this plugin.
 	options := []service.PluginOption{
 		service.WithPrinter(handlePrint),
+		service.WithObjectStatus(handleStatus),
 		service.WithTabPrinter(handleTab),
 		service.WithNavigation(handleNavigation, initRoutes),
 		service.WithActionHandler(handleAction),
@@ -91,6 +93,39 @@ func handleTab(request *service.PrintRequest) (plugin.TabResponse, error) {
 	return plugin.TabResponse{Tab: tab}, nil
 }
 
+func handleStatus(request *service.PrintRequest) (plugin.ObjectStatusResponse, error) {
+	if request.Object == nil {
+		return plugin.ObjectStatusResponse{}, errors.Errorf("object is nil")
+	}
+	// load an object from the cluster and use that object to create a response.
+
+	// Octant has a helper function to generate a key from an object. The key
+	// is used to find the object in the cluster.
+	key, err := store.KeyFromObject(request.Object)
+	if err != nil {
+		return plugin.ObjectStatusResponse{}, err
+	}
+	u, err := request.DashboardClient.Get(request.Context(), key)
+	if err != nil {
+		return plugin.ObjectStatusResponse{}, err
+	}
+
+	// The plugin can check if the object it requested exists.
+	if u == nil {
+		return plugin.ObjectStatusResponse{}, errors.New("object doesn't exist")
+	}
+
+	// Will add object UID to the Resource Viewer properties table
+	return plugin.ObjectStatusResponse{
+		ObjectStatus: component.PodSummary{
+			Properties: []component.Property{{
+				Label: "ID (from plugin)",
+				Value: component.NewText(string(u.GetUID())),
+			}},
+		},
+	}, nil
+}
+
 // handlePrint is called when Octant wants to print an object.
 func handlePrint(request *service.PrintRequest) (plugin.PrintResponse, error) {
 	if request.Object == nil {
diff --git a/internal/modules/workloads/detail_describer.go b/internal/modules/workloads/detail_describer.go
index 7c917a4b..ebb456e7 100644
--- a/internal/modules/workloads/detail_describer.go
+++ b/internal/modules/workloads/detail_describer.go
@@ -41,7 +41,7 @@ func (d *DetailDescriber) loadWorkloads(ctx context.Context, namespace string, o
 		return nil, fmt.Errorf("create workload loader")
 	}
 
-	workloads, err := loader.Load(ctx, namespace)
+	workloads, err := loader.Load(ctx, namespace, options.Link)
 	if err != nil {
 		return nil, fmt.Errorf("load workloads")
 	}
diff --git a/internal/modules/workloads/home_describer.go b/internal/modules/workloads/home_describer.go
index 226adf8f..f5d69cce 100644
--- a/internal/modules/workloads/home_describer.go
+++ b/internal/modules/workloads/home_describer.go
@@ -49,7 +49,7 @@ func loadCards(ctx context.Context, namespace string, options describer.Options)
 		return nil, false, fmt.Errorf("create card collector")
 	}
 
-	cards, fullMetrics, err := collector.Collect(ctx, namespace)
+	cards, fullMetrics, err := collector.Collect(ctx, namespace, options.Link)
 	if err != nil {
 		return nil, false, fmt.Errorf("collect workload cards: %w", err)
 
diff --git a/internal/objectstatus/apiservice.go b/internal/objectstatus/apiservice.go
index ea8ee9f7..dfa35ab9 100644
--- a/internal/objectstatus/apiservice.go
+++ b/internal/objectstatus/apiservice.go
@@ -8,6 +8,8 @@ package objectstatus
 import (
 	"context"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes/scheme"
@@ -19,7 +21,7 @@ import (
 
 // apiService creates status for an apiregistration.k8s.io/v1 apiservice.
 // This is not the final implementation. It is included to generate output.
-func apiService(_ context.Context, object runtime.Object, _ store.Store) (ObjectStatus, error) {
+func apiService(_ context.Context, object runtime.Object, _ store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("apiservice is nil")
 	}
diff --git a/internal/objectstatus/cronjob.go b/internal/objectstatus/cronjob.go
index f39bc2f6..03e9ae3e 100644
--- a/internal/objectstatus/cronjob.go
+++ b/internal/objectstatus/cronjob.go
@@ -8,16 +8,20 @@ package objectstatus
 import (
 	"context"
 
+	"strconv"
+
 	"github.com/pkg/errors"
 	batchv1beta1 "k8s.io/api/batch/v1beta1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes/scheme"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/vmware-tanzu/octant/pkg/store"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
-func cronJob(_ context.Context, object runtime.Object, _ store.Store) (ObjectStatus, error) {
+func cronJob(_ context.Context, object runtime.Object, _ store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("cronjob is nil")
 	}
@@ -28,14 +32,19 @@ func cronJob(_ context.Context, object runtime.Object, _ store.Store) (ObjectSta
 		return ObjectStatus{}, errors.Wrap(err, "convert object to batch/v1beta1 cronjob")
 	}
 
+	properties := []component.Property{{Label: "Schedule", Value: component.NewText(cronjob.Spec.Schedule)}}
+
 	if cronjob.Spec.Suspend != nil && *cronjob.Spec.Suspend {
+		properties = append(properties, component.Property{Label: "Suspend", Value: component.NewText(strconv.FormatBool(*cronjob.Spec.Suspend))})
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusWarning,
 			Details:    []component.Component{component.NewText("Cronjob is suspended")},
+			Properties: properties,
 		}, nil
 	}
 	return ObjectStatus{
 		nodeStatus: component.NodeStatusOK,
 		Details:    []component.Component{component.NewText("batch/v1beta1 CronJob is OK")},
+		Properties: properties,
 	}, nil
 }
diff --git a/internal/objectstatus/daemonset.go b/internal/objectstatus/daemonset.go
index 105aeb3e..3f247df0 100644
--- a/internal/objectstatus/daemonset.go
+++ b/internal/objectstatus/daemonset.go
@@ -8,6 +8,10 @@ package objectstatus
 import (
 	"context"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/pkg/errors"
 	appsv1 "k8s.io/api/apps/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -17,7 +21,7 @@ import (
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
-func daemonSet(_ context.Context, object runtime.Object, _ store.Store) (ObjectStatus, error) {
+func daemonSet(_ context.Context, object runtime.Object, _ store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("daemon set is nil")
 	}
@@ -30,21 +34,65 @@ func daemonSet(_ context.Context, object runtime.Object, _ store.Store) (ObjectS
 
 	status := ds.Status
 
+	var properties []component.Property
+
+	properties = append(properties, component.Property{Label: "Selectors", Value: GetSelectors(ds.Spec.Selector)})
+
+	if nodeSelector := ds.Spec.Template.Spec.NodeSelector; nodeSelector != nil {
+		properties = append(properties, component.Property{Label: "Node Selectors", Value: getSelectorMap(nodeSelector)})
+	}
+
 	switch {
 	case status.NumberMisscheduled > 0:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusWarning,
 			Details:    []component.Component{component.NewText("Daemon Set pods are running on nodes that aren't supposed to run Daemon Set pods")},
+			Properties: properties,
 		}, nil
 	case status.DesiredNumberScheduled != status.NumberReady:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusWarning,
 			Details:    []component.Component{component.NewText("Daemon Set pods are not ready")},
+			Properties: properties,
 		}, nil
 	default:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusOK,
 			Details:    []component.Component{component.NewText("Daemon Set is OK")},
+			Properties: properties,
 		}, nil
 	}
 }
+
+func GetSelectors(selector *metav1.LabelSelector) component.Component {
+	if selector != nil {
+		selectorComponent := component.NewSelectors(nil)
+		if selector == nil {
+			return selectorComponent
+		}
+
+		for k, v := range selector.MatchLabels {
+			selectorComponent.Add(component.NewLabelSelector(k, v))
+		}
+
+		for _, e := range selector.MatchExpressions {
+			es := component.NewExpressionSelector(e.Key, component.Operator(e.Operator), e.Values)
+			selectorComponent.Add(es)
+		}
+		return selectorComponent
+	}
+	return component.NewText("None")
+}
+
+func getSelectorMap(selector map[string]string) *component.Selectors {
+	s := component.NewSelectors(nil)
+	if len(selector) == 0 {
+		return s
+	}
+
+	for k, v := range selector {
+		s.Add(component.NewLabelSelector(k, v))
+	}
+
+	return s
+}
diff --git a/internal/objectstatus/deployment.go b/internal/objectstatus/deployment.go
index c33b3701..220fed24 100644
--- a/internal/objectstatus/deployment.go
+++ b/internal/objectstatus/deployment.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/pkg/errors"
 	appsv1 "k8s.io/api/apps/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -20,7 +22,7 @@ import (
 
 // deploymentAppsV1 creates status for an v1/apps deployment. This is
 // not the final implementation. It is included to generate output.
-func deploymentAppsV1(_ context.Context, object runtime.Object, _ store.Store) (ObjectStatus, error) {
+func deploymentAppsV1(_ context.Context, object runtime.Object, _ store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("deployment is nil")
 	}
@@ -32,17 +34,21 @@ func deploymentAppsV1(_ context.Context, object runtime.Object, _ store.Store) (
 	}
 
 	status := deployment.Status
+	properties := []component.Property{{Label: "Deployment Strategy", Value: component.NewText(string(deployment.Spec.Strategy.Type))},
+		{Label: "Selectors", Value: GetSelectors(deployment.Spec.Selector)}}
 
 	switch {
 	case status.Replicas == status.UnavailableReplicas:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusError,
 			Details:    []component.Component{component.NewText("No replicas exist for this deployment")},
+			Properties: properties,
 		}, nil
 	case status.Replicas == status.AvailableReplicas:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusOK,
 			Details:    []component.Component{component.NewText("Deployment is OK")},
+			Properties: properties,
 		}, nil
 	default:
 		return ObjectStatus{
@@ -51,6 +57,7 @@ func deploymentAppsV1(_ context.Context, object runtime.Object, _ store.Store) (
 				component.NewText(
 					fmt.Sprintf("Expected %d replicas, but %d are available",
 						status.Replicas, status.AvailableReplicas))},
+			Properties: properties,
 		}, nil
 	}
 }
diff --git a/internal/objectstatus/ingress.go b/internal/objectstatus/ingress.go
index b692f5df..190c609a 100644
--- a/internal/objectstatus/ingress.go
+++ b/internal/objectstatus/ingress.go
@@ -9,6 +9,9 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+	"github.com/vmware-tanzu/octant/pkg/view/component"
+
 	"github.com/gobwas/glob"
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
@@ -24,7 +27,7 @@ const (
 	ingressAlbActionAnnotation = "alb.ingress.kubernetes.io/actions."
 )
 
-func runIngressStatus(ctx context.Context, object runtime.Object, o store.Store) (ObjectStatus, error) {
+func runIngressStatus(ctx context.Context, object runtime.Object, o store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("ingress is nil")
 	}
@@ -163,6 +166,12 @@ func (is *ingressStatus) run(ctx context.Context) (ObjectStatus, error) {
 		}
 	}
 
+	var backendText = "Not configured"
+	if backend := ingress.Spec.DefaultBackend; backend != nil {
+		backendText = backend.Service.Name
+	}
+
+	status.Properties = []component.Property{{Label: "Default Backend", Value: component.NewText(backendText)}}
 	return status, nil
 }
 
diff --git a/internal/objectstatus/job.go b/internal/objectstatus/job.go
index c815e306..5769fe16 100644
--- a/internal/objectstatus/job.go
+++ b/internal/objectstatus/job.go
@@ -14,10 +14,14 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes/scheme"
 
+	"github.com/vmware-tanzu/octant/internal/conversion"
+	"github.com/vmware-tanzu/octant/internal/link"
+	"github.com/vmware-tanzu/octant/pkg/view/component"
+
 	"github.com/vmware-tanzu/octant/pkg/store"
 )
 
-func runJobStatus(_ context.Context, object runtime.Object, o store.Store) (ObjectStatus, error) {
+func runJobStatus(_ context.Context, object runtime.Object, o store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("job is nil")
 	}
@@ -57,6 +61,10 @@ func runJobStatus(_ context.Context, object runtime.Object, o store.Store) (Obje
 		os.AddDetail("Job is in progress")
 	}
 
+	properties := []component.Property{{Label: "Completions", Value: component.NewText(conversion.PtrInt32ToString(job.Spec.Completions))},
+		{Label: "Parallelism", Value: component.NewText(conversion.PtrInt32ToString(job.Spec.Parallelism))}}
+
+	os.Properties = properties
 	return os, nil
 }
 
diff --git a/internal/objectstatus/objectstatus.go b/internal/objectstatus/objectstatus.go
index a0753aa8..38e6fcf6 100644
--- a/internal/objectstatus/objectstatus.go
+++ b/internal/objectstatus/objectstatus.go
@@ -9,10 +9,12 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strconv"
 
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/runtime"
 
+	"github.com/vmware-tanzu/octant/internal/link"
 	"github.com/vmware-tanzu/octant/pkg/store"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
@@ -22,7 +24,7 @@ type statusKey struct {
 	kind       string
 }
 
-type statusFunc func(context.Context, runtime.Object, store.Store) (ObjectStatus, error)
+type statusFunc func(context.Context, runtime.Object, store.Store, link.Interface) (ObjectStatus, error)
 
 type statusLookup map[statusKey]statusFunc
 
@@ -46,12 +48,27 @@ var (
 type ObjectStatus struct {
 	nodeStatus component.NodeStatus
 	Details    []component.Component
+	Properties []component.Property
 }
 
 func (os *ObjectStatus) AddDetail(detail string) {
 	os.Details = append(os.Details, component.NewText(detail))
 }
 
+func (os *ObjectStatus) AddProperty(label string, value component.Component) {
+	os.Properties = append(os.Properties, component.Property{
+		Label: label,
+		Value: value,
+	})
+}
+
+func (os *ObjectStatus) InsertProperty(label string, value component.Component) {
+	os.Properties = append([]component.Property{{
+		Label: label,
+		Value: value,
+	}}, os.Properties...)
+}
+
 func (os *ObjectStatus) AddDetailf(msg string, args ...interface{}) {
 	os.AddDetail(fmt.Sprintf(msg, args...))
 }
@@ -78,11 +95,11 @@ func (os *ObjectStatus) Status() component.NodeStatus {
 }
 
 // Status creates an ObjectStatus for an object.
-func Status(ctx context.Context, object runtime.Object, o store.Store) (ObjectStatus, error) {
-	return status(ctx, object, o, defaultStatusLookup)
+func Status(ctx context.Context, object runtime.Object, o store.Store, link link.Interface) (ObjectStatus, error) {
+	return status(ctx, object, o, defaultStatusLookup, link)
 }
 
-func status(ctx context.Context, object runtime.Object, o store.Store, lookup statusLookup) (ObjectStatus, error) {
+func status(ctx context.Context, object runtime.Object, o store.Store, lookup statusLookup, link link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.New("object is nil")
 	}
@@ -108,14 +125,42 @@ func status(ctx context.Context, object runtime.Object, o store.Store, lookup st
 		return ObjectStatus{}, errors.New("status lookup is nil")
 	}
 
+	var oStatus ObjectStatus
 	fn, ok := lookup[statusKey{apiVersion: apiVersion, kind: kind}]
-	if !ok {
-		return ObjectStatus{
+
+	if ok {
+		oStatus, err = fn(ctx, object, o, link)
+	} else {
+		oStatus = ObjectStatus{
 			nodeStatus: component.NodeStatusOK,
 			Details:    []component.Component{component.NewText(fmt.Sprintf("%s %s is OK", apiVersion, kind))},
-		}, nil
+			Properties: []component.Property{},
+		}
+	}
 
+	if link != nil {
+		events, err := store.EventsForObject(ctx, object, o)
+		if err == nil && len(events.Items) > 0 {
+			var lastEvent = events.Items[0]
+			for i := 1; i < len(events.Items); i++ {
+				a, err := strconv.Atoi(events.Items[i].ResourceVersion)
+				b, err := strconv.Atoi(lastEvent.ResourceVersion)
+				if err == nil && a > b {
+					lastEvent = events.Items[i]
+				}
+			}
+
+			if &lastEvent != nil {
+				messageLink, err := link.ForObject(&lastEvent, lastEvent.Message)
+				if err == nil {
+					oStatus.InsertProperty("Last Event", messageLink)
+				}
+			}
+		}
 	}
 
-	return fn(ctx, object, o)
+	oStatus.InsertProperty("Created", component.NewTimestamp(accessor.GetCreationTimestamp().Time))
+	oStatus.InsertProperty("Namespace", component.NewText(accessor.GetNamespace()))
+
+	return oStatus, nil
 }
diff --git a/internal/objectstatus/persistent_volume.go b/internal/objectstatus/persistent_volume.go
index c9ab7f4c..d6215f7e 100644
--- a/internal/objectstatus/persistent_volume.go
+++ b/internal/objectstatus/persistent_volume.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -18,7 +20,7 @@ import (
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
-func persistentVolume(ctx context.Context, object runtime.Object, o store.Store) (ObjectStatus, error) {
+func persistentVolume(ctx context.Context, object runtime.Object, o store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("cronjob is nil")
 	}
diff --git a/internal/objectstatus/pod.go b/internal/objectstatus/pod.go
index dbb96525..23bc0132 100644
--- a/internal/objectstatus/pod.go
+++ b/internal/objectstatus/pod.go
@@ -8,16 +8,20 @@ package objectstatus
 import (
 	"context"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes/scheme"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/vmware-tanzu/octant/pkg/store"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
-func pod(ctx context.Context, object runtime.Object, o store.Store) (ObjectStatus, error) {
+func pod(ctx context.Context, object runtime.Object, o store.Store, link link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("pod is nil")
 	}
@@ -48,5 +52,29 @@ func pod(ctx context.Context, object runtime.Object, o store.Store) (ObjectStatu
 		status.Details = append(status.Details, component.NewText("Ephemeral container is running"))
 	}
 
+	if link != nil {
+		serviceAccountLink, _ := link.ForGVK(pod.Namespace, "v1", "ServiceAccount", pod.Spec.ServiceAccountName, pod.Spec.ServiceAccountName)
+		status.Properties = []component.Property{{Label: "ServiceAccount", Value: serviceAccountLink}}
+
+		if nodeName := pod.Spec.NodeName; nodeName != "" {
+			nodeLink, _ := link.ForGVK("", "v1", "Node", pod.Spec.NodeName, pod.Spec.NodeName)
+			status.AddProperty("Node", nodeLink)
+		}
+
+		ownerReference := metav1.GetControllerOf(pod)
+		if ownerReference != nil {
+			controlledBy, err := link.ForGVK(
+				pod.Namespace,
+				ownerReference.APIVersion,
+				ownerReference.Kind,
+				ownerReference.Name,
+				ownerReference.Name,
+			)
+			if err == nil {
+				status.AddProperty("Controlled By", controlledBy)
+			}
+		}
+	}
+
 	return status, nil
 }
diff --git a/internal/objectstatus/replicaset.go b/internal/objectstatus/replicaset.go
index fef9d958..e7d4d0a7 100644
--- a/internal/objectstatus/replicaset.go
+++ b/internal/objectstatus/replicaset.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/pkg/errors"
 	appsv1 "k8s.io/api/apps/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -20,7 +22,7 @@ import (
 
 // replicaSetAppsV1 creates status for an v1/apps replica set. This is
 // not the final implementation. It is included to generate output.
-func replicaSetAppsV1(_ context.Context, object runtime.Object, _ store.Store) (ObjectStatus, error) {
+func replicaSetAppsV1(_ context.Context, object runtime.Object, _ store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("replica set is nil")
 	}
@@ -36,21 +38,29 @@ func replicaSetAppsV1(_ context.Context, object runtime.Object, _ store.Store) (
 	if r := replicaSet.Spec.Replicas; r != nil {
 		specReplicas = *r
 	}
+	current := fmt.Sprintf("%d", replicaSet.Status.ReadyReplicas)
+	desired := fmt.Sprintf("%d", specReplicas)
+
+	properties := []component.Property{{Label: "Current Replicas", Value: component.NewText(current)},
+		{Label: "Desired Replicas", Value: component.NewText(desired)}}
 
 	switch {
 	case status.Replicas == 0 && specReplicas != 0:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusError,
 			Details:    []component.Component{component.NewText("Replica Set has no replicas available")},
+			Properties: properties,
 		}, nil
 	case status.Replicas == status.AvailableReplicas:
 		return ObjectStatus{nodeStatus: component.NodeStatusOK,
-			Details: []component.Component{component.NewText("Replica Set is OK")},
+			Details:    []component.Component{component.NewText("Replica Set is OK")},
+			Properties: properties,
 		}, nil
 	default:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusWarning,
 			Details:    []component.Component{component.NewText(fmt.Sprintf("Expected %d replicas, but %d are available", status.Replicas, status.AvailableReplicas))},
+			Properties: properties,
 		}, nil
 	}
 
diff --git a/internal/objectstatus/replicationcontroller.go b/internal/objectstatus/replicationcontroller.go
index 92940be0..3c6f6899 100644
--- a/internal/objectstatus/replicationcontroller.go
+++ b/internal/objectstatus/replicationcontroller.go
@@ -8,6 +8,8 @@ package objectstatus
 import (
 	"context"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -17,7 +19,7 @@ import (
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
-func replicationController(_ context.Context, object runtime.Object, _ store.Store) (ObjectStatus, error) {
+func replicationController(_ context.Context, object runtime.Object, _ store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("replication controller is nil")
 	}
diff --git a/internal/objectstatus/service.go b/internal/objectstatus/service.go
index 13a798b2..a3579e44 100644
--- a/internal/objectstatus/service.go
+++ b/internal/objectstatus/service.go
@@ -8,6 +8,8 @@ package objectstatus
 import (
 	"context"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -17,7 +19,7 @@ import (
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
-func service(ctx context.Context, object runtime.Object, o store.Store) (ObjectStatus, error) {
+func service(ctx context.Context, object runtime.Object, o store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("service is nil")
 	}
@@ -63,9 +65,12 @@ func service(ctx context.Context, object runtime.Object, o store.Store) (ObjectS
 			}, nil
 		}
 	}
+	properties := []component.Property{{Label: "Type", Value: component.NewText(string(service.Spec.Type))},
+		{Label: "Session Affinity", Value: component.NewText(string(service.Spec.SessionAffinity))}}
 
 	return ObjectStatus{
 		nodeStatus: component.NodeStatusOK,
 		Details:    []component.Component{component.NewText("Service is OK")},
+		Properties: properties,
 	}, nil
 }
diff --git a/internal/objectstatus/statefulset.go b/internal/objectstatus/statefulset.go
index 7c08bd6a..c6d72e16 100644
--- a/internal/objectstatus/statefulset.go
+++ b/internal/objectstatus/statefulset.go
@@ -7,6 +7,9 @@ package objectstatus
 
 import (
 	"context"
+	"fmt"
+
+	"github.com/vmware-tanzu/octant/internal/link"
 
 	"github.com/pkg/errors"
 	appsv1 "k8s.io/api/apps/v1"
@@ -17,7 +20,7 @@ import (
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 )
 
-func statefulSet(_ context.Context, object runtime.Object, _ store.Store) (ObjectStatus, error) {
+func statefulSet(_ context.Context, object runtime.Object, _ store.Store, _ link.Interface) (ObjectStatus, error) {
 	if object == nil {
 		return ObjectStatus{}, errors.Errorf("stateful set is nil")
 	}
@@ -29,17 +32,24 @@ func statefulSet(_ context.Context, object runtime.Object, _ store.Store) (Objec
 	}
 
 	status := ss.Status
+	total := fmt.Sprintf("%d", status.Replicas)
+	desired := fmt.Sprintf("%d", *ss.Spec.Replicas)
+
+	properties := []component.Property{{Label: "Replicas", Value: component.NewText(fmt.Sprintf("%s Desired / %s Total", desired, total))},
+		{Label: "Pod Management Policy", Value: component.NewText(string(ss.Spec.PodManagementPolicy))}}
 
 	switch {
 	case status.ReadyReplicas != status.Replicas:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusWarning,
 			Details:    []component.Component{component.NewText("Stateful Set pods are not ready")},
+			Properties: properties,
 		}, nil
 	default:
 		return ObjectStatus{
 			nodeStatus: component.NodeStatusOK,
 			Details:    []component.Component{component.NewText("Stateful Set is OK")},
+			Properties: properties,
 		}, nil
 	}
 }
diff --git a/internal/octant/workload.go b/internal/octant/workload.go
index 0e9d2c02..616ac312 100644
--- a/internal/octant/workload.go
+++ b/internal/octant/workload.go
@@ -11,6 +11,8 @@ import (
 	"sort"
 	"sync"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+
 	"github.com/vmware-tanzu/octant/internal/util/path_util"
 
 	corev1 "k8s.io/api/core/v1"
@@ -219,7 +221,7 @@ func addResourceList(a corev1.ResourceList, q resource.Quantity, resourceName co
 // WorkloadLoaderInterface loads workloads from a namespace.
 type WorkloadLoaderInterface interface {
 	// Load loads workloads from a namespace.
-	Load(ctx context.Context, namespace string) ([]Workload, error)
+	Load(ctx context.Context, namespace string, link link.Interface) ([]Workload, error)
 }
 
 // WorkloadCardCollector creates cards for workloads in a namespace.
@@ -239,8 +241,8 @@ func NewWorkloadCardCollector(loader WorkloadLoaderInterface) (*WorkloadCardColl
 }
 
 // Collect collects cards.
-func (wc *WorkloadCardCollector) Collect(ctx context.Context, namespace string) ([]*component.Card, bool, error) {
-	workloads, err := wc.WorkloadLoader.Load(ctx, namespace)
+func (wc *WorkloadCardCollector) Collect(ctx context.Context, namespace string, link link.Interface) ([]*component.Card, bool, error) {
+	workloads, err := wc.WorkloadLoader.Load(ctx, namespace, link)
 	if err != nil {
 		return nil, false, fmt.Errorf("load workloads: %w", err)
 	}
@@ -320,7 +322,7 @@ type ClusterWorkloadLoaderOption func(wl *ClusterWorkloadLoader)
 
 // ClusterWorkloadLoader loads workloads from a Kubernetes cluster.
 type ClusterWorkloadLoader struct {
-	ObjectStatuser   func(context.Context, runtime.Object, store.Store) (objectstatus.ObjectStatus, error)
+	ObjectStatuser   func(context.Context, runtime.Object, store.Store, link.Interface) (objectstatus.ObjectStatus, error)
 	ObjectStore      store.Store
 	PodMetricsLoader PodMetricsLoader
 }
@@ -349,7 +351,7 @@ func NewClusterWorkloadLoader(objectStore store.Store, pml PodMetricsLoader, opt
 }
 
 // Load loads workloads from a cluster.
-func (wl *ClusterWorkloadLoader) Load(ctx context.Context, namespace string) ([]Workload, error) {
+func (wl *ClusterWorkloadLoader) Load(ctx context.Context, namespace string, link link.Interface) ([]Workload, error) {
 	if namespace == "" {
 		return nil, fmt.Errorf("namespace is blank")
 	}
@@ -370,7 +372,7 @@ func (wl *ClusterWorkloadLoader) Load(ctx context.Context, namespace string) ([]
 	for i := range podList.Items {
 		object := &podList.Items[i]
 
-		status, err := wl.ObjectStatuser(ctx, object, wl.ObjectStore)
+		status, err := wl.ObjectStatuser(ctx, object, wl.ObjectStore, link)
 		if err != nil {
 			return nil, fmt.Errorf("get status for pod '%s': %w", object.GetName(), err)
 		}
diff --git a/internal/printer/event.go b/internal/printer/event.go
index 04cb7505..544e1a61 100644
--- a/internal/printer/event.go
+++ b/internal/printer/event.go
@@ -14,10 +14,8 @@ import (
 
 	"github.com/pkg/errors"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/runtime"
 
-	"github.com/vmware-tanzu/octant/internal/util/kubernetes"
 	"github.com/vmware-tanzu/octant/pkg/store"
 	"github.com/vmware-tanzu/octant/pkg/view/component"
 	"github.com/vmware-tanzu/octant/pkg/view/flexlayout"
@@ -228,7 +226,7 @@ func formatEventSource(es corev1.EventSource) string {
 
 func createEventsForObject(ctx context.Context, fl *flexlayout.FlexLayout, object runtime.Object, opts Options) error {
 	objectStore := opts.DashConfig.ObjectStore()
-	eventList, err := eventsForObject(ctx, object, objectStore)
+	eventList, err := store.EventsForObject(ctx, object, objectStore)
 	if err != nil {
 		return errors.Wrap(err, "list events for object")
 	}
@@ -247,73 +245,3 @@ func createEventsForObject(ctx context.Context, fl *flexlayout.FlexLayout, objec
 
 	return nil
 }
-
-func eventsForObject(ctx context.Context, object runtime.Object, o store.Store) (*corev1.EventList, error) {
-	accessor := meta.NewAccessor()
-
-	namespace, err := accessor.Namespace(object)
-	if err != nil {
-		return nil, errors.Wrap(err, "get namespace for object")
-	}
-
-	apiVersion, err := accessor.APIVersion(object)
-	if err != nil {
-		return nil, errors.Wrap(err, "Get apiVersion for object")
-	}
-
-	kind, err := accessor.Kind(object)
-	if err != nil {
-		return nil, errors.Wrap(err, "get kind for object")
-	}
-
-	name, err := accessor.Name(object)
-	if err != nil {
-		return nil, errors.Wrap(err, "get name for object")
-	}
-
-	key := store.Key{
-		Namespace:  namespace,
-		APIVersion: "v1",
-		Kind:       "Event",
-	}
-
-	list, _, err := o.List(ctx, key)
-	if err != nil {
-		return nil, errors.Wrap(err, "list events for object")
-	}
-
-	eventList := &corev1.EventList{}
-
-	for _, unstructuredEvent := range list.Items {
-		event := &corev1.Event{}
-		err := kubernetes.FromUnstructured(&unstructuredEvent, event)
-		if err != nil {
-			return nil, err
-		}
-
-		involvedObject := event.InvolvedObject
-		if involvedObject.APIVersion == "autoscaling/v2beta2" || involvedObject.APIVersion == "autoscaling/v2beta1" {
-			involvedObject.APIVersion = "autoscaling/v1"
-		}
-
-		if involvedObject.Namespace == namespace &&
-			involvedObject.APIVersion == apiVersion &&
-			involvedObject.Kind == kind &&
-			involvedObject.Name == name {
-			eventList.Items = append(eventList.Items, *event)
-		}
-	}
-
-	sort.SliceStable(eventList.Items, func(i, j int) bool {
-		a := eventList.Items[i]
-		b := eventList.Items[j]
-
-		if b.LastTimestamp.After(a.LastTimestamp.Time) {
-			return true
-		}
-
-		return a.LastTimestamp.After(b.LastTimestamp.Time)
-	})
-
-	return eventList, nil
-}
diff --git a/internal/printer/object_table.go b/internal/printer/object_table.go
index 47e6b3b6..fd903b9a 100644
--- a/internal/printer/object_table.go
+++ b/internal/printer/object_table.go
@@ -69,7 +69,7 @@ func (ol *ObjectTable) AddRowForObject(ctx context.Context, object runtime.Objec
 		row["_isDeleted"] = component.NewText("deleted")
 	}
 
-	status, err := objectstatus.Status(ctx, object, ol.store)
+	status, err := objectstatus.Status(ctx, object, ol.store, nil)
 	if err != nil {
 		return fmt.Errorf("get status for object: %w", err)
 	}
diff --git a/internal/resourceviewer/fake/mock_object_status.go b/internal/resourceviewer/fake/mock_object_status.go
index c5bdee6c..447cbf1c 100644
--- a/internal/resourceviewer/fake/mock_object_status.go
+++ b/internal/resourceviewer/fake/mock_object_status.go
@@ -11,6 +11,7 @@ import (
 	gomock "github.com/golang/mock/gomock"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 
+	link "github.com/vmware-tanzu/octant/internal/link"
 	objectstatus "github.com/vmware-tanzu/octant/internal/objectstatus"
 )
 
@@ -38,16 +39,16 @@ func (m *MockObjectStatus) EXPECT() *MockObjectStatusMockRecorder {
 }
 
 // Status mocks base method
-func (m *MockObjectStatus) Status(arg0 context.Context, arg1 runtime.Object) (*objectstatus.ObjectStatus, error) {
+func (m *MockObjectStatus) Status(arg0 context.Context, arg1 runtime.Object, arg2 link.Interface) (*objectstatus.ObjectStatus, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Status", arg0, arg1)
+	ret := m.ctrl.Call(m, "Status", arg0, arg1, arg2)
 	ret0, _ := ret[0].(*objectstatus.ObjectStatus)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // Status indicates an expected call of Status
-func (mr *MockObjectStatusMockRecorder) Status(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockObjectStatusMockRecorder) Status(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockObjectStatus)(nil).Status), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockObjectStatus)(nil).Status), arg0, arg1, arg2)
 }
diff --git a/internal/resourceviewer/handler.go b/internal/resourceviewer/handler.go
index ac5d5e3b..10ce1c12 100644
--- a/internal/resourceviewer/handler.go
+++ b/internal/resourceviewer/handler.go
@@ -285,7 +285,7 @@ func (h *Handler) Nodes(ctx context.Context) (component.Nodes, error) {
 		pgn := podGroupNode{
 			objectStatus: h.objectStatus,
 		}
-		group, err := pgn.Create(ctx, podGroupName, objects)
+		group, err := pgn.Create(ctx, podGroupName, objects, h.link)
 		if err != nil {
 			return nil, err
 		}
@@ -415,7 +415,7 @@ func isObjectParent(child, parent runtime.Object) (bool, error) {
 }
 
 type ObjectStatus interface {
-	Status(ctx context.Context, object runtime.Object) (*objectstatus.ObjectStatus, error)
+	Status(ctx context.Context, object runtime.Object, link link.Interface) (*objectstatus.ObjectStatus, error)
 }
 
 type HandlerObjectStatus struct {
@@ -432,8 +432,8 @@ func NewHandlerObjectStatus(objectStore store.Store, pluginManager plugin.Manage
 	}
 }
 
-func (h *HandlerObjectStatus) Status(ctx context.Context, object runtime.Object) (*objectstatus.ObjectStatus, error) {
-	status, err := objectstatus.Status(ctx, object, h.objectStore)
+func (h *HandlerObjectStatus) Status(ctx context.Context, object runtime.Object, link link.Interface) (*objectstatus.ObjectStatus, error) {
+	status, err := objectstatus.Status(ctx, object, h.objectStore, link)
 	if err != nil {
 		return nil, err
 	}
@@ -444,6 +444,7 @@ func (h *HandlerObjectStatus) Status(ctx context.Context, object runtime.Object)
 	}
 
 	status.Details = append(status.Details, pluginStatus.ObjectStatus.Details...)
+	status.Properties = append(status.Properties, pluginStatus.ObjectStatus.Properties...)
 
 	return &status, nil
 }
diff --git a/internal/resourceviewer/object_node.go b/internal/resourceviewer/object_node.go
index 0d78db73..90c4cf85 100644
--- a/internal/resourceviewer/object_node.go
+++ b/internal/resourceviewer/object_node.go
@@ -49,7 +49,7 @@ func (o *objectNode) Create(ctx context.Context, object *unstructured.Unstructur
 		return nil, err
 	}
 
-	status, err := o.objectStatus.Status(ctx, object)
+	status, err := o.objectStatus.Status(ctx, object, o.link)
 	if err != nil {
 		return nil, err
 	}
@@ -59,6 +59,7 @@ func (o *objectNode) Create(ctx context.Context, object *unstructured.Unstructur
 		APIVersion: apiVersion,
 		Kind:       kind,
 		Status:     status.Status(),
+		Properties: status.Properties,
 		Details:    status.Details,
 		Path:       objectPath,
 	}
diff --git a/internal/resourceviewer/pod_group_node.go b/internal/resourceviewer/pod_group_node.go
index 2bc7a2bf..047a3116 100644
--- a/internal/resourceviewer/pod_group_node.go
+++ b/internal/resourceviewer/pod_group_node.go
@@ -2,6 +2,9 @@ package resourceviewer
 
 import (
 	"context"
+	"sort"
+
+	"github.com/vmware-tanzu/octant/internal/link"
 
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 
@@ -12,10 +15,15 @@ type podGroupNode struct {
 	objectStatus ObjectStatus
 }
 
-func (pgn *podGroupNode) Create(ctx context.Context, podGroupName string, objects []unstructured.Unstructured) (*component.Node, error) {
+func (pgn *podGroupNode) Create(ctx context.Context, podGroupName string, objects []unstructured.Unstructured, link link.Interface) (*component.Node, error) {
 	podStatus := component.NewPodStatus()
+	var podProperties []component.Property
+
+	sort.Slice(objects, func(i, j int) bool {
+		return objects[i].GetName() < objects[j].GetName()
+	})
 
-	for _, object := range objects {
+	for i, object := range objects {
 		if !isObjectPod(&object) {
 			continue
 		}
@@ -24,12 +32,15 @@ func (pgn *podGroupNode) Create(ctx context.Context, podGroupName string, object
 			return nil, err
 		}
 
-		status, err := pgn.objectStatus.Status(ctx, &object)
+		status, err := pgn.objectStatus.Status(ctx, &object, link)
 		if err != nil {
 			return nil, err
 		}
 
-		podStatus.AddSummary(pod.Name, status.Details, status.Status())
+		podStatus.AddSummary(pod.Name, status.Details, status.Properties, status.Status())
+		if i == 0 { // we add Properties that are common for all pods and only once
+			podProperties = append(podProperties, status.Properties...)
+		}
 	}
 
 	node := &component.Node{
@@ -37,6 +48,7 @@ func (pgn *podGroupNode) Create(ctx context.Context, podGroupName string, object
 		APIVersion: "v1",
 		Kind:       "Pod",
 		Status:     podStatus.Status(),
+		Properties: podProperties,
 		Details:    []component.Component{podStatus},
 	}
 	return node, nil
diff --git a/pkg/store/utils.go b/pkg/store/utils.go
new file mode 100644
index 00000000..19143deb
--- /dev/null
+++ b/pkg/store/utils.go
@@ -0,0 +1,82 @@
+package store
+
+import (
+	"context"
+	"sort"
+
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/runtime"
+
+	"github.com/vmware-tanzu/octant/internal/util/kubernetes"
+)
+
+func EventsForObject(ctx context.Context, object runtime.Object, o Store) (*corev1.EventList, error) {
+	accessor := meta.NewAccessor()
+	namespace, err := accessor.Namespace(object)
+	if err != nil {
+		return nil, errors.Wrap(err, "get namespace for object")
+	}
+
+	apiVersion, err := accessor.APIVersion(object)
+	if err != nil {
+		return nil, errors.Wrap(err, "Get apiVersion for object")
+	}
+
+	kind, err := accessor.Kind(object)
+	if err != nil {
+		return nil, errors.Wrap(err, "get kind for object")
+	}
+
+	name, err := accessor.Name(object)
+	if err != nil {
+		return nil, errors.Wrap(err, "get name for object")
+	}
+
+	key := Key{
+		Namespace:  namespace,
+		APIVersion: "v1",
+		Kind:       "Event",
+	}
+
+	list, _, err := o.List(ctx, key)
+	if err != nil {
+		return nil, errors.Wrap(err, "list events for object")
+	}
+
+	eventList := &corev1.EventList{}
+
+	for _, unstructuredEvent := range list.Items {
+		event := &corev1.Event{}
+		err := kubernetes.FromUnstructured(&unstructuredEvent, event)
+		if err != nil {
+			return nil, err
+		}
+
+		involvedObject := event.InvolvedObject
+		if involvedObject.APIVersion == "autoscaling/v2beta2" || involvedObject.APIVersion == "autoscaling/v2beta1" {
+			involvedObject.APIVersion = "autoscaling/v1"
+		}
+
+		if involvedObject.Namespace == namespace &&
+			involvedObject.APIVersion == apiVersion &&
+			involvedObject.Kind == kind &&
+			involvedObject.Name == name {
+			eventList.Items = append(eventList.Items, *event)
+		}
+	}
+
+	sort.SliceStable(eventList.Items, func(i, j int) bool {
+		a := eventList.Items[i]
+		b := eventList.Items[j]
+
+		if b.LastTimestamp.After(a.LastTimestamp.Time) {
+			return true
+		}
+
+		return a.LastTimestamp.After(b.LastTimestamp.Time)
+	})
+
+	return eventList, nil
+}
diff --git a/pkg/view/component/pod_status.go b/pkg/view/component/pod_status.go
index 9e58e4ad..e33f48fc 100644
--- a/pkg/view/component/pod_status.go
+++ b/pkg/view/component/pod_status.go
@@ -9,8 +9,9 @@ import "github.com/vmware-tanzu/octant/internal/util/json"
 
 // PodSummary is a status summary for a pod.
 type PodSummary struct {
-	Details []Component `json:"details,omitempty"`
-	Status  NodeStatus  `json:"status,omitempty"`
+	Details    []Component `json:"details,omitempty"`
+	Properties []Property  `json:"properties,omitempty"`
+	Status     NodeStatus  `json:"status,omitempty"`
 }
 
 // PodStatusConfig is config for PodStatus.
@@ -48,10 +49,11 @@ func (ps *PodStatus) MarshalJSON() ([]byte, error) {
 }
 
 // AddSummary adds summary for a pod.
-func (ps *PodStatus) AddSummary(name string, details []Component, status NodeStatus) {
+func (ps *PodStatus) AddSummary(name string, details []Component, properties []Property, status NodeStatus) {
 	ps.Config.Pods[name] = PodSummary{
-		Details: details,
-		Status:  status,
+		Details:    details,
+		Properties: properties,
+		Status:     status,
 	}
 }
 
@@ -74,8 +76,9 @@ func (ps *PodStatus) Status() NodeStatus {
 
 func (podSummary *PodSummary) UnmarshalJSON(data []byte) error {
 	stage := struct {
-		Details []TypedObject `json:"details,omitempty"`
-		Status  NodeStatus    `json:"status,omitempty"`
+		Details    []TypedObject    `json:"details,omitempty"`
+		Properties []PropertyObject `json:"properties,omitempty"`
+		Status     NodeStatus       `json:"status,omitempty"`
 	}{}
 
 	if err := json.Unmarshal(data, &stage); err != nil {
@@ -93,5 +96,14 @@ func (podSummary *PodSummary) UnmarshalJSON(data []byte) error {
 		podSummary.Details = append(podSummary.Details, status)
 	}
 
+	for _, to := range stage.Properties {
+		val, err := to.Value.ToComponent()
+		if err != nil {
+			return err
+		}
+
+		podSummary.Properties = append(podSummary.Properties, Property{Label: to.Label, Value: val})
+	}
+
 	return nil
 }
diff --git a/pkg/view/component/resource_viewer.go b/pkg/view/component/resource_viewer.go
index a09cd3ed..d9c1cc12 100644
--- a/pkg/view/component/resource_viewer.go
+++ b/pkg/view/component/resource_viewer.go
@@ -66,17 +66,29 @@ type Node struct {
 	Kind       string      `json:"kind,omitempty"`
 	Status     NodeStatus  `json:"status,omitempty"`
 	Details    []Component `json:"details,omitempty"`
+	Properties []Property  `json:"properties,omitempty"`
 	Path       *Link       `json:"path,omitempty"`
 }
 
+type Property struct {
+	Label string    `json:"label,omitempty"`
+	Value Component `json:"value,omitempty"`
+}
+
+type PropertyObject struct {
+	Label string       `json:"label,omitempty"`
+	Value *TypedObject `json:"value,omitempty"`
+}
+
 func (n *Node) UnmarshalJSON(data []byte) error {
 	x := struct {
-		Name       string         `json:"name,omitempty"`
-		APIVersion string         `json:"apiVersion,omitempty"`
-		Kind       string         `json:"kind,omitempty"`
-		Status     NodeStatus     `json:"status,omitempty"`
-		Details    []*TypedObject `json:"details,omitempty"`
-		Path       *TypedObject   `json:"path,omitempty"`
+		Name       string           `json:"name,omitempty"`
+		APIVersion string           `json:"apiVersion,omitempty"`
+		Kind       string           `json:"kind,omitempty"`
+		Status     NodeStatus       `json:"status,omitempty"`
+		Details    []*TypedObject   `json:"details,omitempty"`
+		Properties []PropertyObject `json:"properties,omitempty"`
+		Path       *TypedObject     `json:"path,omitempty"`
 	}{}
 
 	if err := json.Unmarshal(data, &x); err != nil {
@@ -99,6 +111,18 @@ func (n *Node) UnmarshalJSON(data []byte) error {
 		n.Details[i] = dc
 	}
 
+	if x.Properties != nil {
+		n.Properties = make([]Property, len(x.Properties))
+	}
+	for i, property := range x.Properties {
+		comp, err := property.Value.ToComponent()
+		if err != nil {
+			return errors.Wrap(err, "unmarshal-ing property")
+		}
+		prop := Property{property.Label, comp}
+		n.Properties[i] = prop
+	}
+
 	if x.Path != nil {
 		p, err := x.Path.ToComponent()
 		if err != nil {
diff --git a/web/package-lock.json b/web/package-lock.json
index fbdedf10..2402ba3a 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -9296,9 +9296,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "14.14.43",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.43.tgz",
-      "integrity": "sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ==",
+      "version": "14.17.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.1.tgz",
+      "integrity": "sha512-/tpUyFD7meeooTRwl3sYlihx2BrJE7q9XF71EguPFIySj9B7qgnRtHsHTho+0AUm4m1SvWGm6uSncrR94q6Vtw==",
       "dev": true
     },
     "@types/node-fetch": {
diff --git a/web/src/app/modules/shared/components/presentation/cytoscape/cytoscape.component.ts b/web/src/app/modules/shared/components/presentation/cytoscape/cytoscape.component.ts
index 43bbb4f1..82e9f463 100644
--- a/web/src/app/modules/shared/components/presentation/cytoscape/cytoscape.component.ts
+++ b/web/src/app/modules/shared/components/presentation/cytoscape/cytoscape.component.ts
@@ -55,7 +55,9 @@ export class CytoscapeComponent implements OnChanges, OnDestroy {
   }
 
   ngOnChanges(changes: SimpleChanges): void {
-    this.render();
+    if (changes.zoom || changes.elements || changes.style || changes.layout) {
+      this.render();
+    }
   }
 
   ngOnDestroy(): void {
diff --git a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.html b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.html
index 74e51e65..87ff0764 100644
--- a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.html
+++ b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.html
@@ -20,4 +20,25 @@
       </div>
     </div>
   </div>
+  <div class="properties">
+    <div>
+      <table class="table table-vertical">
+      <tbody>
+      <tr >
+        <th>Version</th>
+        <td>{{node.apiVersion}}</td>
+      </tr>
+      <tr>
+        <th>Kind</th>
+        <td>{{node.kind}}</td>
+      </tr>
+      <tr *ngFor="let prop of node.properties; trackBy: propertiesTrackBy">
+        <th>{{prop.label}}</th>
+        <td><app-view-container [view]="prop.value"></app-view-container></td>
+      </tr>
+      </tbody>
+    </table>
+    </div>
+  </div>
+
 </div>
diff --git a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.scss b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.scss
index 25b0ba74..d58f0989 100644
--- a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.scss
+++ b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.scss
@@ -9,3 +9,33 @@
 .node-status {
   list-style-type: none;
 }
+
+:host ::ng-deep .properties table {
+  width: 100%;
+  table-layout: fixed;
+
+  th {
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  td {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: block;
+
+    .selectors-container, .labels-container {
+      display: block;
+    }
+
+    .selectors-container .label, .labels-container .label {
+      margin-top: 4px;
+      margin-bottom: 0px;
+    }
+  }
+}
+
+.status div.card div.card-block div {
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
diff --git a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.spec.ts b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.spec.ts
index 79b82207..56e15e8c 100644
--- a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.spec.ts
+++ b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.spec.ts
@@ -9,6 +9,7 @@ import {
   OverlayScrollbarsComponent,
   OverlayscrollbarsModule,
 } from 'overlayscrollbars-ngx';
+import { View } from '../../../models/content';
 
 describe('ObjectStatusComponent', () => {
   let component: ObjectStatusComponent;
@@ -32,4 +33,34 @@ describe('ObjectStatusComponent', () => {
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  it('should show properties table', () => {
+    component.node = {
+      apiVersion: 'v1',
+      details: [],
+      kind: 'Pod',
+      name: 'test-pod',
+      path: undefined,
+      properties: [
+        {
+          label: 'test',
+          value: {
+            metadata: { type: 'text' },
+            config: { value: 'property text' },
+          } as View,
+        },
+      ],
+      status: 'pod status',
+    };
+    fixture.detectChanges();
+
+    const root: HTMLElement = fixture.nativeElement;
+    const el: SVGPathElement = root.querySelector('.properties');
+
+    expect(component).toBeTruthy();
+    expect(el).not.toBeNull();
+    expect(el.innerHTML).toContain('Pod');
+    expect(el.innerHTML).toContain('v1');
+    expect(el.innerHTML).toContain('property text');
+  });
 });
diff --git a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.ts b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.ts
index 956c1e9f..ff5af4bf 100644
--- a/web/src/app/modules/shared/components/presentation/object-status/object-status.component.ts
+++ b/web/src/app/modules/shared/components/presentation/object-status/object-status.component.ts
@@ -28,7 +28,11 @@ export class ObjectStatusComponent {
     ];
   }
 
-  detailsTrackBy(index, item) {
+  detailsTrackBy(index, _) {
+    return index;
+  }
+
+  propertiesTrackBy(index, _) {
     return index;
   }
 }
diff --git a/web/src/app/modules/shared/models/content.ts b/web/src/app/modules/shared/models/content.ts
index 7b1b093c..32d673d4 100644
--- a/web/src/app/modules/shared/models/content.ts
+++ b/web/src/app/modules/shared/models/content.ts
@@ -282,12 +282,18 @@ export interface Edge {
   edge: string;
 }
 
+export interface Property {
+  label: string;
+  value: View;
+}
+
 export interface Node {
   name: string;
   apiVersion: string;
   kind: string;
   status: string;
   details: View[];
+  properties: Property[];
   path: LinkView;
 }
 


Test patch:
diff --git a/internal/objectstatus/apiservice_test.go b/internal/objectstatus/apiservice_test.go
index 1c80caf7..ec3ee2f8 100644
--- a/internal/objectstatus/apiservice_test.go
+++ b/internal/objectstatus/apiservice_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -84,6 +86,7 @@ func Test_apiService(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storeFake.NewMockStore(controller)
@@ -91,7 +94,7 @@ func Test_apiService(t *testing.T) {
 			object := tc.init(t, o)
 
 			ctx := context.Background()
-			status, err := apiService(ctx, object, o)
+			status, err := apiService(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/daemonset_test.go b/internal/objectstatus/daemonset_test.go
index beb6d45b..6988993f 100644
--- a/internal/objectstatus/daemonset_test.go
+++ b/internal/objectstatus/daemonset_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -37,6 +39,7 @@ func Test_daemonSet(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusOK,
 				Details:    []component.Component{component.NewText("Daemon Set is OK")},
+				Properties: []component.Property{{Label: "Selectors", Value: component.NewSelectors([]component.Selector{component.NewLabelSelector("name", "fluentd")})}},
 			},
 		},
 		{
@@ -49,6 +52,7 @@ func Test_daemonSet(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusWarning,
 				Details:    []component.Component{component.NewText("Daemon Set pods are running on nodes that aren't supposed to run Daemon Set pods")},
+				Properties: []component.Property{{Label: "Selectors", Value: component.NewSelectors([]component.Selector{component.NewLabelSelector("name", "fluentd")})}},
 			},
 		},
 		{
@@ -61,6 +65,7 @@ func Test_daemonSet(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusWarning,
 				Details:    []component.Component{component.NewText("Daemon Set pods are not ready")},
+				Properties: []component.Property{{Label: "Selectors", Value: component.NewSelectors([]component.Selector{component.NewLabelSelector("name", "fluentd")})}},
 			},
 		},
 		{
@@ -82,6 +87,7 @@ func Test_daemonSet(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storefake.NewMockStore(controller)
@@ -89,7 +95,7 @@ func Test_daemonSet(t *testing.T) {
 			object := tc.init(t, o)
 
 			ctx := context.Background()
-			status, err := daemonSet(ctx, object, o)
+			status, err := daemonSet(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/deployment_test.go b/internal/objectstatus/deployment_test.go
index 3bab019a..169c6914 100644
--- a/internal/objectstatus/deployment_test.go
+++ b/internal/objectstatus/deployment_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -37,6 +39,8 @@ func Test_deploymentAppsV1(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusOK,
 				Details:    []component.Component{component.NewText("Deployment is OK")},
+				Properties: []component.Property{{Label: "Deployment Strategy", Value: component.NewText("RollingUpdate")},
+					{Label: "Selectors", Value: component.NewSelectors([]component.Selector{component.NewLabelSelector("app", "hello-node")})}},
 			},
 		},
 		{
@@ -49,6 +53,8 @@ func Test_deploymentAppsV1(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusError,
 				Details:    []component.Component{component.NewText("No replicas exist for this deployment")},
+				Properties: []component.Property{{Label: "Deployment Strategy", Value: component.NewText("RollingUpdate")},
+					{Label: "Selectors", Value: component.NewSelectors([]component.Selector{component.NewLabelSelector("app", "hello-node")})}},
 			},
 		},
 		{
@@ -61,6 +67,8 @@ func Test_deploymentAppsV1(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusWarning,
 				Details:    []component.Component{component.NewText("Expected 1 replicas, but 0 are available")},
+				Properties: []component.Property{{Label: "Deployment Strategy", Value: component.NewText("RollingUpdate")},
+					{Label: "Selectors", Value: component.NewSelectors([]component.Selector{component.NewLabelSelector("app", "hello-node")})}},
 			},
 		},
 		{
@@ -82,6 +90,7 @@ func Test_deploymentAppsV1(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storeFake.NewMockStore(controller)
@@ -89,7 +98,7 @@ func Test_deploymentAppsV1(t *testing.T) {
 			object := tc.init(t, o)
 
 			ctx := context.Background()
-			status, err := deploymentAppsV1(ctx, object, o)
+			status, err := deploymentAppsV1(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/ingress_test.go b/internal/objectstatus/ingress_test.go
index 59a45290..d717ebff 100644
--- a/internal/objectstatus/ingress_test.go
+++ b/internal/objectstatus/ingress_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -37,7 +39,8 @@ func Test_runIngressStatus(t *testing.T) {
 
 			},
 			expected: ObjectStatus{
-				Details: []component.Component{component.NewText("Ingress is OK")},
+				Details:    []component.Component{component.NewText("Ingress is OK")},
+				Properties: []component.Property{{Label: "Default Backend", Value: component.NewText("single-service")}},
 			},
 		},
 		{
@@ -53,6 +56,7 @@ func Test_runIngressStatus(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusError,
 				Details:    []component.Component{component.NewText("Backend refers to service \"no-such-service\" which doesn't exist")},
+				Properties: []component.Property{{Label: "Default Backend", Value: component.NewText("Not configured")}},
 			},
 		},
 		{
@@ -65,6 +69,7 @@ func Test_runIngressStatus(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusError,
 				Details:    []component.Component{component.NewText("Backend for service \"service-wrong-port\" specifies an invalid port")},
+				Properties: []component.Property{{Label: "Default Backend", Value: component.NewText("Not configured")}},
 			},
 		},
 		{
@@ -80,6 +85,7 @@ func Test_runIngressStatus(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusError,
 				Details:    []component.Component{component.NewText("No matching TLS host for rule \"not-the-tls-host.com\"")},
+				Properties: []component.Property{{Label: "Default Backend", Value: component.NewText("Not configured")}},
 			},
 		},
 		{
@@ -93,7 +99,8 @@ func Test_runIngressStatus(t *testing.T) {
 
 			},
 			expected: ObjectStatus{
-				Details: []component.Component{component.NewText("Ingress is OK")},
+				Details:    []component.Component{component.NewText("Ingress is OK")},
+				Properties: []component.Property{{Label: "Default Backend", Value: component.NewText("Not configured")}},
 			},
 		},
 		{
@@ -111,6 +118,7 @@ func Test_runIngressStatus(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusError,
 				Details:    []component.Component{component.NewText("Secret \"no-such-secret\" does not exist")},
+				Properties: []component.Property{{Label: "Default Backend", Value: component.NewText("Not configured")}},
 			},
 		},
 		{
@@ -132,6 +140,7 @@ func Test_runIngressStatus(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storefake.NewMockStore(controller)
@@ -139,7 +148,7 @@ func Test_runIngressStatus(t *testing.T) {
 			object := tc.init(t, o)
 
 			ctx := context.Background()
-			status, err := runIngressStatus(ctx, object, o)
+			status, err := runIngressStatus(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/job_test.go b/internal/objectstatus/job_test.go
index 0a3b2bcd..efe4bf8b 100644
--- a/internal/objectstatus/job_test.go
+++ b/internal/objectstatus/job_test.go
@@ -9,6 +9,10 @@ import (
 	"context"
 	"testing"
 
+	"github.com/golang/mock/gomock"
+
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -36,6 +40,8 @@ func Test_runJobStatus(t *testing.T) {
 					component.NewText("Job has succeeded 1 time"),
 					component.NewText("Job completed in 11s"),
 				},
+				Properties: []component.Property{{Label: "Completions", Value: component.NewText("1")},
+					{Label: "Parallelism", Value: component.NewText("1")}},
 			},
 		},
 		{
@@ -50,6 +56,8 @@ func Test_runJobStatus(t *testing.T) {
 					component.NewText("Job has failed 2 times"),
 					component.NewText("Job is in progress"),
 				},
+				Properties: []component.Property{{Label: "Completions", Value: component.NewText("1")},
+					{Label: "Parallelism", Value: component.NewText("1")}},
 			},
 		},
 		{
@@ -64,6 +72,8 @@ func Test_runJobStatus(t *testing.T) {
 					component.NewText("Job has failed 5 times"),
 					component.NewText("Job has reached the specified backoff limit"),
 				},
+				Properties: []component.Property{{Label: "Completions", Value: component.NewText("1")},
+					{Label: "Parallelism", Value: component.NewText("1")}},
 			},
 		},
 		{
@@ -85,9 +95,11 @@ func Test_runJobStatus(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			object := tc.init(t)
+			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 
 			ctx := context.Background()
-			status, err := runJobStatus(ctx, object, nil)
+			status, err := runJobStatus(ctx, object, nil, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/objectstatus_test.go b/internal/objectstatus/objectstatus_test.go
index d35fa6d4..77744fcb 100644
--- a/internal/objectstatus/objectstatus_test.go
+++ b/internal/objectstatus/objectstatus_test.go
@@ -9,6 +9,11 @@ import (
 	"context"
 	"testing"
 
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	"github.com/vmware-tanzu/octant/internal/link"
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 
 	"github.com/stretchr/testify/assert"
@@ -22,13 +27,16 @@ import (
 )
 
 func Test_status(t *testing.T) {
+	deployment := testutil.CreateDeployment("deployment")
 	deployObjectStatus := ObjectStatus{
 		nodeStatus: component.NodeStatusOK,
 		Details:    []component.Component{component.NewText("apps/v1 Deployment is OK")},
+		Properties: []component.Property{{Label: "Namespace", Value: component.NewText("namespace")},
+			{Label: "Created", Value: component.NewTimestamp(deployment.CreationTimestamp.Time)}},
 	}
 
 	lookup := statusLookup{
-		{apiVersion: "v1", kind: "Object"}: func(context.Context, runtime.Object, store.Store) (ObjectStatus, error) {
+		{apiVersion: "v1", kind: "Object"}: func(context.Context, runtime.Object, store.Store, link.Interface) (ObjectStatus, error) {
 			return deployObjectStatus, nil
 		},
 	}
@@ -42,7 +50,7 @@ func Test_status(t *testing.T) {
 	}{
 		{
 			name:     "in general",
-			object:   testutil.CreateDeployment("deployment"),
+			object:   deployment,
 			lookup:   lookup,
 			expected: deployObjectStatus,
 		},
@@ -63,12 +71,14 @@ func Test_status(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storefake.NewMockStore(controller)
+			o.EXPECT().List(gomock.Any(), gomock.Any()).Return(&unstructured.UnstructuredList{}, false, nil).AnyTimes()
 
 			ctx := context.Background()
-			got, err := status(ctx, tc.object, o, tc.lookup)
+			got, err := status(ctx, tc.object, o, tc.lookup, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/pod_test.go b/internal/objectstatus/pod_test.go
index 92470a5b..69d85a79 100644
--- a/internal/objectstatus/pod_test.go
+++ b/internal/objectstatus/pod_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -38,6 +40,9 @@ func Test_pod(t *testing.T) {
 				Details: []component.Component{
 					component.NewText(""),
 				},
+				Properties: []component.Property{{Label: "ServiceAccount", Value: component.NewLink("", "ServiceAccount", "some-url/service-account")},
+					{Label: "Node", Value: component.NewLink("", "Node", "some-url/node")},
+					{Label: "Controlled By", Value: component.NewLink("", "ReplicaSet", "some-url/replica")}},
 			},
 		},
 		{
@@ -51,6 +56,9 @@ func Test_pod(t *testing.T) {
 				Details: []component.Component{
 					component.NewText(""),
 				},
+				Properties: []component.Property{{Label: "ServiceAccount", Value: component.NewLink("", "ServiceAccount", "some-url/service-account")},
+					{Label: "Node", Value: component.NewLink("", "Node", "some-url/node")},
+					{Label: "Controlled By", Value: component.NewLink("", "ReplicaSet", "some-url/replica")}},
 			},
 		},
 		{
@@ -64,6 +72,9 @@ func Test_pod(t *testing.T) {
 				Details: []component.Component{
 					component.NewText(""),
 				},
+				Properties: []component.Property{{Label: "ServiceAccount", Value: component.NewLink("", "ServiceAccount", "some-url/service-account")},
+					{Label: "Node", Value: component.NewLink("", "Node", "some-url/node")},
+					{Label: "Controlled By", Value: component.NewLink("", "ReplicaSet", "some-url/replica")}},
 			},
 		},
 		{
@@ -92,6 +103,9 @@ func Test_pod(t *testing.T) {
 					component.NewText(""),
 					component.NewText("Ephemeral container is running"),
 				},
+				Properties: []component.Property{{Label: "ServiceAccount", Value: component.NewLink("", "ServiceAccount", "some-url/service-account")},
+					{Label: "Node", Value: component.NewLink("", "Node", "some-url/node")},
+					{Label: "Controlled By", Value: component.NewLink("", "ReplicaSet", "some-url/replica")}},
 			},
 		},
 	}
@@ -99,6 +113,13 @@ func Test_pod(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
+			linkSA := component.NewLink("", "ServiceAccount", "some-url/service-account")
+			linkNode := component.NewLink("", "Node", "some-url/node")
+			linkReplica := component.NewLink("", "ReplicaSet", "some-url/replica")
+			linkInterface.EXPECT().ForGVK(gomock.Any(), gomock.Any(), "ServiceAccount", gomock.Any(), gomock.Any()).Return(linkSA, nil).AnyTimes()
+			linkInterface.EXPECT().ForGVK(gomock.Any(), gomock.Any(), "Node", gomock.Any(), gomock.Any()).Return(linkNode, nil).AnyTimes()
+			linkInterface.EXPECT().ForGVK(gomock.Any(), gomock.Any(), "ReplicaSet", gomock.Any(), gomock.Any()).Return(linkReplica, nil).AnyTimes()
 			defer controller.Finish()
 
 			o := storefake.NewMockStore(controller)
@@ -106,7 +127,7 @@ func Test_pod(t *testing.T) {
 			object := tc.init(t)
 
 			ctx := context.Background()
-			status, err := pod(ctx, object, o)
+			status, err := pod(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/replicaset_test.go b/internal/objectstatus/replicaset_test.go
index 4122feef..7df11b67 100644
--- a/internal/objectstatus/replicaset_test.go
+++ b/internal/objectstatus/replicaset_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -37,6 +39,8 @@ func Test_replicaSetAppsV1(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusOK,
 				Details:    []component.Component{component.NewText("Replica Set is OK")},
+				Properties: []component.Property{{Label: "Current Replicas", Value: component.NewText("1")},
+					{Label: "Desired Replicas", Value: component.NewText("1")}},
 			},
 		},
 		{
@@ -49,6 +53,8 @@ func Test_replicaSetAppsV1(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusError,
 				Details:    []component.Component{component.NewText("Replica Set has no replicas available")},
+				Properties: []component.Property{{Label: "Current Replicas", Value: component.NewText("1")},
+					{Label: "Desired Replicas", Value: component.NewText("1")}},
 			},
 		},
 		{
@@ -61,6 +67,8 @@ func Test_replicaSetAppsV1(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusOK,
 				Details:    []component.Component{component.NewText("Replica Set is OK")},
+				Properties: []component.Property{{Label: "Current Replicas", Value: component.NewText("0")},
+					{Label: "Desired Replicas", Value: component.NewText("0")}},
 			},
 		},
 		{
@@ -73,6 +81,8 @@ func Test_replicaSetAppsV1(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusWarning,
 				Details:    []component.Component{component.NewText("Expected 1 replicas, but 0 are available")},
+				Properties: []component.Property{{Label: "Current Replicas", Value: component.NewText("1")},
+					{Label: "Desired Replicas", Value: component.NewText("1")}},
 			},
 		},
 		{
@@ -94,6 +104,7 @@ func Test_replicaSetAppsV1(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storefake.NewMockStore(controller)
@@ -101,7 +112,7 @@ func Test_replicaSetAppsV1(t *testing.T) {
 			object := tc.init(t, o)
 
 			ctx := context.Background()
-			status, err := replicaSetAppsV1(ctx, object, o)
+			status, err := replicaSetAppsV1(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/replicationcontroller_test.go b/internal/objectstatus/replicationcontroller_test.go
index 5c48afb0..cdb688d2 100644
--- a/internal/objectstatus/replicationcontroller_test.go
+++ b/internal/objectstatus/replicationcontroller_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -70,6 +72,7 @@ func Test_replicationController(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storefake.NewMockStore(controller)
@@ -77,7 +80,7 @@ func Test_replicationController(t *testing.T) {
 			object := tc.init(t, o)
 
 			ctx := context.Background()
-			status, err := replicationController(ctx, object, o)
+			status, err := replicationController(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/service_test.go b/internal/objectstatus/service_test.go
index 8bdce246..fd6392e9 100644
--- a/internal/objectstatus/service_test.go
+++ b/internal/objectstatus/service_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -50,6 +52,8 @@ func Test_service(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusOK,
 				Details:    []component.Component{component.NewText("Service is OK")},
+				Properties: []component.Property{{Label: "Type", Value: component.NewText("ClusterIP")},
+					{Label: "Session Affinity", Value: component.NewText("None")}},
 			},
 		},
 		{
@@ -61,6 +65,8 @@ func Test_service(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusOK,
 				Details:    []component.Component{component.NewText("Service is OK")},
+				Properties: []component.Property{{Label: "Type", Value: component.NewText("ExternalName")},
+					{Label: "Session Affinity", Value: component.NewText("")}},
 			},
 		},
 		{
@@ -106,6 +112,7 @@ func Test_service(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storefake.NewMockStore(controller)
@@ -113,7 +120,7 @@ func Test_service(t *testing.T) {
 			object := tc.init(t, o)
 
 			ctx := context.Background()
-			status, err := service(ctx, object, o)
+			status, err := service(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/objectstatus/statefulset_test.go b/internal/objectstatus/statefulset_test.go
index 67c6fd38..5fc57a54 100644
--- a/internal/objectstatus/statefulset_test.go
+++ b/internal/objectstatus/statefulset_test.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -37,6 +39,8 @@ func Test_statefulSet(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusOK,
 				Details:    []component.Component{component.NewText("Stateful Set is OK")},
+				Properties: []component.Property{{Label: "Replicas", Value: component.NewText("3 Desired / 3 Total")},
+					{Label: "Pod Management Policy", Value: component.NewText("OrderedReady")}},
 			},
 		},
 		{
@@ -49,6 +53,8 @@ func Test_statefulSet(t *testing.T) {
 			expected: ObjectStatus{
 				nodeStatus: component.NodeStatusWarning,
 				Details:    []component.Component{component.NewText("Stateful Set pods are not ready")},
+				Properties: []component.Property{{Label: "Replicas", Value: component.NewText("3 Desired / 3 Total")},
+					{Label: "Pod Management Policy", Value: component.NewText("OrderedReady")}},
 			},
 		},
 		{
@@ -70,6 +76,7 @@ func Test_statefulSet(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			controller := gomock.NewController(t)
+			linkInterface := linkFake.NewMockInterface(controller)
 			defer controller.Finish()
 
 			o := storefake.NewMockStore(controller)
@@ -77,7 +84,7 @@ func Test_statefulSet(t *testing.T) {
 			object := tc.init(t, o)
 
 			ctx := context.Background()
-			status, err := statefulSet(ctx, object, o)
+			status, err := statefulSet(ctx, object, o, linkInterface)
 			if tc.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/octant/workload_test.go b/internal/octant/workload_test.go
index fb5c8dea..cb2ce062 100644
--- a/internal/octant/workload_test.go
+++ b/internal/octant/workload_test.go
@@ -10,6 +10,9 @@ import (
 	"fmt"
 	"testing"
 
+	"github.com/vmware-tanzu/octant/internal/link"
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -226,8 +229,10 @@ func TestClusterWorkloadLoader(t *testing.T) {
 	for _, c := range cases {
 		t.Run(c.name, func(t *testing.T) {
 			ctx := context.Background()
+			linkInterface := linkFake.NewMockInterface(controller)
+
 			wl, err := octant.NewClusterWorkloadLoader(c.objectStore, c.podMetricLoader, func(wl *octant.ClusterWorkloadLoader) {
-				wl.ObjectStatuser = func(context.Context, runtime.Object, store.Store) (status objectstatus.ObjectStatus, e error) {
+				wl.ObjectStatuser = func(context.Context, runtime.Object, store.Store, link.Interface) (status objectstatus.ObjectStatus, e error) {
 					objectStatus := objectstatus.ObjectStatus{}
 					return objectStatus, nil
 
@@ -235,7 +240,7 @@ func TestClusterWorkloadLoader(t *testing.T) {
 			})
 			require.NoError(t, err)
 
-			actual, err := wl.Load(ctx, c.namespace)
+			actual, err := wl.Load(ctx, c.namespace, linkInterface)
 			if c.isErr {
 				require.Error(t, err)
 				return
diff --git a/internal/printer/event_test.go b/internal/printer/event_test.go
index f486f132..8753bec2 100644
--- a/internal/printer/event_test.go
+++ b/internal/printer/event_test.go
@@ -384,7 +384,7 @@ func Test_eventsForObject(t *testing.T) {
 	o.EXPECT().List(gomock.Any(), gomock.Eq(key)).Return(events, false, nil)
 
 	ctx := context.Background()
-	got, err := eventsForObject(ctx, object, o)
+	got, err := store.EventsForObject(ctx, object, o)
 	require.NoError(t, err)
 
 	expected := &corev1.EventList{
diff --git a/internal/resourceviewer/handler_test.go b/internal/resourceviewer/handler_test.go
index e876bd5f..417464c3 100644
--- a/internal/resourceviewer/handler_test.go
+++ b/internal/resourceviewer/handler_test.go
@@ -81,7 +81,7 @@ func TestHandler(t *testing.T) {
 
 	objectStatus := fake.NewMockObjectStatus(controller)
 	objectStatus.EXPECT().
-		Status(gomock.Any(), gomock.Any()).
+		Status(gomock.Any(), gomock.Any(), gomock.Any()).
 		Return(&objectstatus.ObjectStatus{}, nil).
 		AnyTimes()
 
@@ -170,11 +170,11 @@ func TestHandler(t *testing.T) {
 	}
 
 	podStatus1 := component.NewPodStatus()
-	podStatus1.AddSummary(pod1.Name, nil, component.NodeStatusOK)
-	podStatus1.AddSummary(pod2.Name, nil, component.NodeStatusOK)
+	podStatus1.AddSummary(pod1.Name, nil, nil, component.NodeStatusOK)
+	podStatus1.AddSummary(pod2.Name, nil, nil, component.NodeStatusOK)
 
 	podStatus2 := component.NewPodStatus()
-	podStatus2.AddSummary(pod4.Name, nil, component.NodeStatusOK)
+	podStatus2.AddSummary(pod4.Name, nil, nil, component.NodeStatusOK)
 
 	expectedNodes := component.Nodes{
 		string(cr.UID): {
diff --git a/internal/resourceviewer/object_node_test.go b/internal/resourceviewer/object_node_test.go
index e8731372..e6633867 100644
--- a/internal/resourceviewer/object_node_test.go
+++ b/internal/resourceviewer/object_node_test.go
@@ -30,7 +30,7 @@ func Test_objectNode(t *testing.T) {
 	pluginPrinter := pluginFake.NewMockManagerInterface(controller)
 	objectStatus := fake.NewMockObjectStatus(controller)
 	objectStatus.EXPECT().
-		Status(gomock.Any(), gomock.Any()).
+		Status(gomock.Any(), gomock.Any(), gomock.Any()).
 		Return(&objectstatus.ObjectStatus{}, nil)
 
 	on := objectNode{
diff --git a/internal/resourceviewer/pod_group_node_test.go b/internal/resourceviewer/pod_group_node_test.go
index b0c89c62..ed9753f9 100644
--- a/internal/resourceviewer/pod_group_node_test.go
+++ b/internal/resourceviewer/pod_group_node_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"testing"
 
+	linkFake "github.com/vmware-tanzu/octant/internal/link/fake"
+
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/require"
 
@@ -15,12 +17,13 @@ import (
 
 func Test_podGroupNode(t *testing.T) {
 	controller := gomock.NewController(t)
+	linkInterface := linkFake.NewMockInterface(controller)
 	defer controller.Finish()
 
 	pod := testutil.ToUnstructured(t, testutil.CreatePod("pod"))
 	objectStatus := fake.NewMockObjectStatus(controller)
 	objectStatus.EXPECT().
-		Status(gomock.Any(), pod).
+		Status(gomock.Any(), pod, gomock.Any()).
 		Return(&objectstatus.ObjectStatus{}, nil)
 
 	pgn := podGroupNode{objectStatus: objectStatus}
@@ -30,11 +33,11 @@ func Test_podGroupNode(t *testing.T) {
 
 	ctx := context.Background()
 
-	got, err := pgn.Create(ctx, name, objects.Items)
+	got, err := pgn.Create(ctx, name, objects.Items, linkInterface)
 	require.NoError(t, err)
 
 	podStatus := component.NewPodStatus()
-	podStatus.AddSummary(pod.GetName(), nil, component.NodeStatusOK)
+	podStatus.AddSummary(pod.GetName(), nil, nil, component.NodeStatusOK)
 
 	expected := &component.Node{
 		Name:       "foo pods",
diff --git a/pkg/view/component/pod_status_test.go b/pkg/view/component/pod_status_test.go
index f0564f85..61b985d3 100644
--- a/pkg/view/component/pod_status_test.go
+++ b/pkg/view/component/pod_status_test.go
@@ -56,7 +56,7 @@ func Test_PodStatus_Marshal(t *testing.T) {
 func Test_PodStatus_AddSummary(t *testing.T) {
 	podStatus := NewPodStatus()
 
-	podStatus.AddSummary("name", []Component{NewText("details")}, NodeStatusOK)
+	podStatus.AddSummary("name", []Component{NewText("details")}, nil, NodeStatusOK)
 
 	expected := NewPodStatus()
 	expected.Config.Pods["name"] = PodSummary{
@@ -93,7 +93,7 @@ func Test_PodStatus_Status(t *testing.T) {
 			podStatus := NewPodStatus()
 
 			for i, status := range tc.statuses {
-				podStatus.AddSummary(fmt.Sprintf("%d", i), []Component{NewText("details")}, status)
+				podStatus.AddSummary(fmt.Sprintf("%d", i), []Component{NewText("details")}, nil, status)
 			}
 
 			assert.Equal(t, tc.expected, podStatus.Status())