Repository: vmware-tanzu/octant
Pull request #801 (2020-03-29 17:21:46)

Create grid action
Signed-off-by: bryanl <bryanliles@gmail.com>

**What this PR does / why we need it**:

Create grid action that allows users of the table component to add grid actions. It allows Octant to take advantage of https://clarity.design/documentation/datagrid/single-action

<img width="892" alt="Screen Shot 2020-03-29 at 1 20 43 PM" src="https://user-images.githubusercontent.com/240/77855709-219fc600-71c0-11ea-90ec-9944c3858a5d.png">

**Which issue(s) this PR fixes**
None

**Release note**:
```
Add support for Clarity's single action for data grid rows
```



Patch:
diff --git a/changelogs/unreleased/801-bryanl b/changelogs/unreleased/801-bryanl
new file mode 100644
index 00000000..1fc2acda
--- /dev/null
+++ b/changelogs/unreleased/801-bryanl
@@ -0,0 +1,1 @@
+Add support for Clarity's single action for data grid rows
diff --git a/pkg/view/component/base.go b/pkg/view/component/base.go
index ef948f87..4c59abdd 100644
--- a/pkg/view/component/base.go
+++ b/pkg/view/component/base.go
@@ -19,6 +19,7 @@ const (
 	typeExpressionSelector  = "expressionSelector"
 	typeFlexLayout          = "flexlayout"
 	typeGraphviz            = "graphviz"
+	typeGridActions         = "gridActions"
 	typeIFrame              = "iframe"
 	typeLabels              = "labels"
 	typeLabelSelector       = "labelSelector"
diff --git a/pkg/view/component/grid_actions.go b/pkg/view/component/grid_actions.go
new file mode 100644
index 00000000..bdbece5c
--- /dev/null
+++ b/pkg/view/component/grid_actions.go
@@ -0,0 +1,71 @@
+/*
+ *  Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+ *  SPDX-License-Identifier: Apache-2.0
+ *
+ */
+
+package component
+
+import (
+	"encoding/json"
+
+	"github.com/vmware-tanzu/octant/pkg/action"
+)
+
+type GridAction struct {
+	// Name is the name of action. It will be shown to the user.
+	Name string `json:"name"`
+	// ActionPath is the path of the action.
+	ActionPath string `json:"actionPath"`
+	// Payload is the payload that will be submitted with the action is invoked.
+	Payload action.Payload `json:"payload"`
+}
+
+// GridActions add the ability to have specific actions for rows. This will allow for dynamic injection of actions
+// that could be dependent on the content of a grid row.
+type GridActions struct {
+	base
+
+	Config GridActionsConfig `json:"config"`
+}
+
+var _ Component = &GridActions{}
+
+// NewGridActions creates an instance of GridActions.
+func NewGridActions() *GridActions {
+	a := GridActions{
+		base: newBase(typeGridActions, nil),
+	}
+
+	return &a
+}
+
+// AddAction adds an action to GridAction.
+func (a *GridActions) AddAction(name, actionPath string, payload action.Payload) {
+	ga := GridAction{
+		Name:       name,
+		ActionPath: actionPath,
+		Payload:    payload,
+	}
+
+	a.Config.Actions = append(a.Config.Actions, ga)
+}
+
+type gridActionsMarshal GridActions
+
+// MarshalJSON converts the GridActions to a JSON.
+func (a GridActions) MarshalJSON() ([]byte, error) {
+	m := gridActionsMarshal{
+		base:   a.base,
+		Config: a.Config,
+	}
+
+	m.Metadata.Type = typeGridActions
+	return json.Marshal(&m)
+}
+
+// GridActionsConfig is configuration items for GridActions.
+type GridActionsConfig struct {
+	// Actions is a slice that contains actions that can be performed by the user.
+	Actions []GridAction `json:"actions"`
+}
diff --git a/pkg/view/component/unmarshal.go b/pkg/view/component/unmarshal.go
index 1aea6b78..8b09dbf0 100644
--- a/pkg/view/component/unmarshal.go
+++ b/pkg/view/component/unmarshal.go
@@ -76,6 +76,11 @@ func unmarshal(to TypedObject) (Component, error) {
 		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
 			"unmarshal graphviz config")
 		o = t
+	case typeGridActions:
+		t := &GridActions{base: base{Metadata: to.Metadata}}
+		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
+			"unmarshal gridActions config")
+		o = t
 	case typeIFrame:
 		t := &IFrame{base: base{Metadata: to.Metadata}}
 		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
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 f01651c6..91fe1ff8 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,16 +17,21 @@
             ></app-content-filter>
         </clr-dg-filter>
     </clr-dg-column>
-    <clr-dg-row *clrDgItems="let row of rows; trackBy: identifyRow">
+    <clr-dg-row *clrDgItems="let row of rowsWithMetadata; trackBy: identifyRow">
+        <clr-dg-action-overflow *ngIf="row.actions.length > 0">
+            <button *ngFor="let action of row.actions" class="action-item" (click)="runAction(action.actionPath, action.payload)">
+                {{action.name}}
+            </button>
+        </clr-dg-action-overflow>
         <clr-dg-cell *ngFor="let column of columns; trackBy: identifyColumn">
-            <app-content-switcher [view]="row[column]"></app-content-switcher>
+            <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]">Items per page</clr-dg-page-size>
-            <ng-container *ngIf="rows?.length > 0">
+            <ng-container *ngIf="rowsWithMetadata?.length > 0">
                 {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
                 of {{pagination.totalItems}} items
             </ng-container>
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 8f57eac1..7a40b35f 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
@@ -4,6 +4,8 @@
 
 import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
 import {
+  GridAction,
+  GridActionsView,
   TableFilters,
   TableRow,
   TableView,
@@ -12,6 +14,7 @@ import {
 import trackByIndex from 'src/app/util/trackBy/trackByIndex';
 import trackByIdentity from 'src/app/util/trackBy/trackByIdentity';
 import { ViewService } from '../../../services/view/view.service';
+import { ActionService } from '../../../services/action/action.service';
 
 @Component({
   selector: 'app-view-datagrid',
@@ -29,7 +32,7 @@ export class DatagridComponent implements OnChanges {
   }
 
   columns: string[];
-  rows: TableRow[];
+  rowsWithMetadata: TableRowWithMetadata[];
   title: string;
   placeholder: string;
   lastUpdated: Date;
@@ -41,7 +44,10 @@ export class DatagridComponent implements OnChanges {
   identifyColumn = trackByIdentity;
   loading: boolean;
 
-  constructor(private viewService: ViewService) {}
+  constructor(
+    private viewService: ViewService,
+    private actionService: ActionService
+  ) {}
 
   ngOnChanges(changes: SimpleChanges): void {
     if (changes.view) {
@@ -51,9 +57,10 @@ export class DatagridComponent implements OnChanges {
       ) {
         this.title = this.viewService.viewTitleAsText(this.view);
 
-        const current = changes.view.currentValue;
+        const current = changes.view.currentValue as TableView;
         this.columns = current.config.columns.map(column => column.name);
-        this.rows = current.config.rows;
+        this.rowsWithMetadata = this.getRowsWithMetadata(current.config.rows);
+
         this.placeholder = current.config.emptyContent;
         this.lastUpdated = new Date();
         this.loading = current.config.loading;
@@ -63,4 +70,28 @@ export class DatagridComponent implements OnChanges {
       }
     }
   }
+
+  private getRowsWithMetadata(rows: TableRow[]) {
+    return rows.map(row => {
+      let actions: GridAction[] = [];
+
+      if (row.hasOwnProperty('_action')) {
+        actions = (row._action as GridActionsView).config.actions;
+      }
+      return {
+        data: row,
+        actions,
+      };
+    });
+  }
+
+  runAction(actionPath: string, payload: {}) {
+    const update = { ...payload, action: actionPath };
+    this.actionService.perform(update);
+  }
+}
+
+interface TableRowWithMetadata {
+  data: TableRow;
+  actions?: GridAction[];
 }
diff --git a/web/src/app/modules/shared/models/content.ts b/web/src/app/modules/shared/models/content.ts
index 01fe78fe..88220f9d 100644
--- a/web/src/app/modules/shared/models/content.ts
+++ b/web/src/app/modules/shared/models/content.ts
@@ -120,6 +120,18 @@ export interface FlexLayoutView extends View {
   };
 }
 
+export interface GridAction {
+  name: string;
+  actionPath: string;
+  payload: {};
+}
+
+export interface GridActionsView extends View {
+  config: {
+    actions: GridAction[];
+  };
+}
+
 export interface LabelsView extends View {
   config: {
     labels: { [key: string]: string };


Test patch:
diff --git a/pkg/view/component/grid_actions_test.go b/pkg/view/component/grid_actions_test.go
new file mode 100644
index 00000000..4f83f5a9
--- /dev/null
+++ b/pkg/view/component/grid_actions_test.go
@@ -0,0 +1,31 @@
+/*
+ *  Copyright (c) 2020 the Octant contributors. All Rights Reserved.
+ *  SPDX-License-Identifier: Apache-2.0
+ *
+ */
+
+package component
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/vmware-tanzu/octant/pkg/action"
+)
+
+func TestGridActions_AddAction(t *testing.T) {
+	ga := NewGridActions()
+
+	payload := action.Payload{"foo": "bar"}
+	ga.AddAction("name", "/path", payload)
+
+	expected := []GridAction{
+		{
+			Name:       "name",
+			ActionPath: "/path",
+			Payload:    payload,
+		},
+	}
+	require.Equal(t, expected, ga.Config.Actions)
+}
diff --git a/pkg/view/component/testdata/config_grid_actions.json b/pkg/view/component/testdata/config_grid_actions.json
new file mode 100644
index 00000000..15cdebe6
--- /dev/null
+++ b/pkg/view/component/testdata/config_grid_actions.json
@@ -0,0 +1,11 @@
+{
+  "actions": [
+    {
+      "name": "name",
+      "actionPath": "/path",
+      "payload": {
+        "foo": "bar"
+      }
+    }
+  ]
+}
diff --git a/pkg/view/component/unmarshal_test.go b/pkg/view/component/unmarshal_test.go
index 07ffab11..189fb32a 100644
--- a/pkg/view/component/unmarshal_test.go
+++ b/pkg/view/component/unmarshal_test.go
@@ -12,6 +12,8 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/vmware-tanzu/octant/pkg/action"
 )
 
 func Test_unmarshal(t *testing.T) {
@@ -161,6 +163,22 @@ func Test_unmarshal(t *testing.T) {
 				base: newBase(typeFlexLayout, nil),
 			},
 		},
+		{
+			name:       "grid actions",
+			configFile: "config_grid_actions.json",
+			objectType: typeGridActions,
+			expected: &GridActions{
+				Config: GridActionsConfig{
+					Actions: []GridAction{
+						{
+							Name:       "name",
+							ActionPath: "/path",
+							Payload:    action.Payload{"foo": "bar"},
+						},
+					},
+				},
+			},
+		},
 		{
 			name:       "labels",
 			configFile: "config_labels.json",