Repository: vmware-tanzu/octant
Pull request #1299 (2020-08-27 01:41:08)

Add a ButtonGroup to the Table
**What this PR does / why we need it**:

Tables support grid actions that perform actions on a given row of the
table, but did not have a way to take an action on the whole table. Now
an optional button group is rendered to the right of the title.

For example, a button could be used to create a new item in the table.

<img width="935" alt="Screen Shot 2020-08-27 at 1 13 28 PM" src="https://user-images.githubusercontent.com/302992/91473614-20ee8700-e867-11ea-833a-8d9021f70691.png">


**Release note**:
```
Add an optional button group to data grid tables
```



Patch:
diff --git a/changelogs/unreleased/1299-scothis b/changelogs/unreleased/1299-scothis
new file mode 100644
index 00000000..7270639e
--- /dev/null
+++ b/changelogs/unreleased/1299-scothis
@@ -0,0 +1,1 @@
+Add an optional button group to data grid tables
diff --git a/pkg/plugin/javascript/dashboard_update.go b/pkg/plugin/javascript/dashboard_update.go
index 8faa30b9..37996bd9 100644
--- a/pkg/plugin/javascript/dashboard_update.go
+++ b/pkg/plugin/javascript/dashboard_update.go
@@ -7,7 +7,6 @@ package javascript
 
 import (
 	"context"
-	"errors"
 
 	"github.com/dop251/goja"
 
@@ -44,7 +43,7 @@ func (d *DashboardUpdate) Call(ctx context.Context, vm *goja.Runtime) func(c goj
 
 		results, err := d.storage.ObjectStore().CreateOrUpdateFromYAML(ctx, namespace, update)
 		if err != nil {
-			panic(panicMessage(vm, errors.New("no YAML was supplied"), ""))
+			panic(panicMessage(vm, err, ""))
 		}
 
 		return vm.ToValue(results)
diff --git a/pkg/view/component/table.go b/pkg/view/component/table.go
index d33e379e..818fa2ea 100644
--- a/pkg/view/component/table.go
+++ b/pkg/view/component/table.go
@@ -12,6 +12,9 @@ import (
 	"sync"
 
 	"github.com/davecgh/go-spew/spew"
+	"github.com/pkg/errors"
+
+	"github.com/vmware-tanzu/octant/pkg/action"
 )
 
 // TableFilter describer a text filter for a table.
@@ -27,6 +30,43 @@ type TableConfig struct {
 	EmptyContent string                 `json:"emptyContent"`
 	Loading      bool                   `json:"loading"`
 	Filters      map[string]TableFilter `json:"filters"`
+	ButtonGroup  *ButtonGroup           `json:"buttonGroup,omitempty"`
+}
+
+func (t *TableConfig) UnmarshalJSON(data []byte) error {
+	x := struct {
+		Columns      []TableCol             `json:"columns"`
+		Rows         []TableRow             `json:"rows"`
+		EmptyContent string                 `json:"emptyContent"`
+		Loading      bool                   `json:"loading"`
+		Filters      map[string]TableFilter `json:"filters"`
+		ButtonGroup  *TypedObject           `json:"buttonGroup,omitempty"`
+	}{}
+
+	if err := json.Unmarshal(data, &x); err != nil {
+		return err
+	}
+
+	if x.ButtonGroup != nil {
+		component, err := x.ButtonGroup.ToComponent()
+		if err != nil {
+			return err
+		}
+
+		buttonGroup, ok := component.(*ButtonGroup)
+		if !ok {
+			return errors.New("item was not a buttonGroup")
+		}
+		t.ButtonGroup = buttonGroup
+	}
+
+	t.Columns = x.Columns
+	t.Rows = x.Rows
+	t.EmptyContent = x.EmptyContent
+	t.Loading = x.Loading
+	t.Filters = x.Filters
+
+	return nil
 }
 
 // TableCol describes a column from a table. Accessor is the key this
@@ -179,6 +219,15 @@ func (t *Table) AddFilter(columnName string, filter TableFilter) {
 	t.Config.Filters[columnName] = filter
 }
 
+// AddButton adds a button the button group for a table.
+func (t *Table) AddButton(name string, payload action.Payload, buttonOptions ...ButtonOption) {
+	if t.Config.ButtonGroup == nil {
+		t.Config.ButtonGroup = NewButtonGroup()
+	}
+	button := NewButton(name, payload, buttonOptions...)
+	t.Config.ButtonGroup.AddButton(button)
+}
+
 // Columns returns the table columns.
 func (t *Table) Columns() []TableCol {
 	return t.Config.Columns
diff --git a/pkg/view/component/unmarshal.go b/pkg/view/component/unmarshal.go
index 6ade26e2..5ea7f17d 100644
--- a/pkg/view/component/unmarshal.go
+++ b/pkg/view/component/unmarshal.go
@@ -7,7 +7,6 @@ package component
 
 import (
 	"encoding/json"
-	"fmt"
 
 	"github.com/pkg/errors"
 )
@@ -139,9 +138,8 @@ func unmarshal(to TypedObject) (Component, error) {
 		o = t
 	case TypeStepper:
 		t := &Stepper{Base: Base{Metadata: to.Metadata}}
-		if uErr := json.Unmarshal(to.Config, &t.Config); uErr != nil {
-			err = fmt.Errorf("unmarshal stepper config: %w", uErr)
-		}
+		err = errors.Wrapf(json.Unmarshal(to.Config, &t.Config),
+			"unmarshal stepper config")
 		o = t
 	case TypeSummary:
 		t := &Summary{Base: Base{Metadata: to.Metadata}}
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 f9611c74..0533a57a 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
@@ -1,4 +1,9 @@
-<h4 *ngIf="showTitle()">{{ title }}</h4>
+<div class="clr-row clr-align-items-center clr-justify-content-between" *ngIf="showTitle() || buttonGroup">
+  <h4 class="clr-col-10" *ngIf="showTitle()">{{ title }}</h4>
+    <clr-dg-action-bar class="clr-col-2" *ngIf="buttonGroup">
+      <app-button-group class="btn-sm" [view]="buttonGroup"></app-button-group>
+    </clr-dg-action-bar>
+</div>
 <clr-datagrid [clrDgLoading]="loading$ | async">
   <clr-dg-placeholder>
     <ng-container *ngIf="placeholder?.length > 0; else emptyPlaceholder">
diff --git a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
index 3729f037..312c2c19 100644
--- a/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
+++ b/web/src/app/modules/shared/components/presentation/datagrid/datagrid.component.scss
@@ -13,3 +13,7 @@
 
   background-color: var(--barberpole-color);
 }
+
+app-button-group {
+  float: right;
+}
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 318e058f..16125e54 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
@@ -25,6 +25,7 @@ import { ActionService } from '../../../services/action/action.service';
 import { AbstractViewComponent } from '../../abstract-view/abstract-view.component';
 import { BehaviorSubject, merge, Observable, timer } from 'rxjs';
 import { LoadingService } from '../../../services/loading/loading.service';
+import { ButtonGroupView } from '../../../models/content';
 
 @Component({
   selector: 'app-view-datagrid',
@@ -42,6 +43,7 @@ export class DatagridComponent extends AbstractViewComponent<TableView> {
   placeholder: string;
   lastUpdated: Date;
   filters: TableFilters;
+  buttonGroup?: ButtonGroupView;
   isModalOpen = false;
 
   actionDialogOptions: ActionDialogOptions = undefined;
@@ -81,6 +83,7 @@ export class DatagridComponent extends AbstractViewComponent<TableView> {
     });
     this.columns = this.v.config.columns.map(column => column.name);
     this.filters = this.v.config.filters;
+    this.buttonGroup = this.v.config.buttonGroup;
   }
 
   private getRowsWithMetadata(rows: TableRow[]): TableRowWithMetadata[] {
diff --git a/web/src/app/modules/shared/models/content.ts b/web/src/app/modules/shared/models/content.ts
index 88d542cf..bd64dea0 100644
--- a/web/src/app/modules/shared/models/content.ts
+++ b/web/src/app/modules/shared/models/content.ts
@@ -302,6 +302,7 @@ export interface TableView extends View {
     emptyContent: string;
     loading: boolean;
     filters: TableFilters;
+    buttonGroup?: ButtonGroupView;
   };
 }
 


Test patch:
diff --git a/pkg/view/component/testdata/config_table_buttongroup.json b/pkg/view/component/testdata/config_table_buttongroup.json
new file mode 100644
index 00000000..d2d1b7e2
--- /dev/null
+++ b/pkg/view/component/testdata/config_table_buttongroup.json
@@ -0,0 +1,66 @@
+{
+  "columns": [
+    {
+      "name": "Name",
+      "accessor": "Name"
+    },
+    {
+      "name": "Description",
+      "accessor": "Description"
+    }
+  ],
+  "rows": [
+    {
+      "Description": {
+        "metadata": {
+          "type": "text"
+        },
+        "config": {
+          "value": "The first row"
+        }
+      },
+      "Name": {
+        "metadata": {
+          "type": "text"
+        },
+        "config": {
+          "value": "First"
+        }
+      }
+    },
+    {
+      "Description": {
+        "metadata": {
+          "type": "text"
+        },
+        "config": {
+          "value": "The last row"
+        }
+      },
+      "Name": {
+        "metadata": {
+          "type": "text"
+        },
+        "config": {
+          "value": "Last"
+        }
+      }
+    }
+  ],
+  "buttonGroup": {
+    "metadata": {
+      "type": "buttonGroup"
+    },
+    "config": {
+      "buttons": [
+        {
+          "name": "Create",
+          "payload": {
+            "action": "action.local/create",
+            "prop": "value"
+          }
+        }
+      ]
+    }
+  }
+}
diff --git a/pkg/view/component/unmarshal_test.go b/pkg/view/component/unmarshal_test.go
index 7b839c74..9ceff5f9 100644
--- a/pkg/view/component/unmarshal_test.go
+++ b/pkg/view/component/unmarshal_test.go
@@ -478,6 +478,60 @@ func Test_unmarshal(t *testing.T) {
 				Base: newBase(TypeTable, nil),
 			},
 		},
+		{
+			name:       "table with button group",
+			configFile: "config_table_buttongroup.json",
+			objectType: "table",
+			expected: &Table{
+				Config: TableConfig{
+					Columns: NewTableCols("Name", "Description"),
+					Rows: []TableRow{
+						{
+							"Description": &Text{
+								Config: TextConfig{
+									Text: "The first row",
+								},
+								Base: newBase(TypeText, nil),
+							},
+							"Name": &Text{
+								Config: TextConfig{
+									Text: "First",
+								},
+								Base: newBase(TypeText, nil),
+							},
+						},
+						{
+							"Description": &Text{
+								Config: TextConfig{
+									Text: "The last row",
+								},
+								Base: newBase(TypeText, nil),
+							},
+							"Name": &Text{
+								Config: TextConfig{
+									Text: "Last",
+								},
+								Base: newBase(TypeText, nil),
+							},
+						},
+					},
+					ButtonGroup: &ButtonGroup{
+						Config: ButtonGroupConfig{
+							Buttons: []Button{
+								{
+									Name: "Create",
+									Payload: action.Payload{
+										"action": "action.local/create",
+										"prop":   "value",
+									},
+								},
+							},
+						},
+					},
+				},
+				Base: newBase(TypeTable, nil),
+			},
+		},
 		{
 			name:       "text",
 			configFile: "config_text.json",