Add Types for Ideal State, fix Payload Format (#2168)

Add types for IdealState in helix-front. Fix an associated JSON serialization bug.
diff --git a/helix-front/.prettierignore b/helix-front/.prettierignore
index ca49e9b..712db9e 100644
--- a/helix-front/.prettierignore
+++ b/helix-front/.prettierignore
@@ -4,3 +4,4 @@
 node_modules
 target
 package.json
+coverage/
diff --git a/helix-front/angular.json b/helix-front/angular.json
index c23dfaf..c81813d 100644
--- a/helix-front/angular.json
+++ b/helix-front/angular.json
@@ -106,10 +106,7 @@
               "node_modules/ace-builds/src-min/mode-json.js"
             ],
             "styles": ["client/styles.scss", "client/theme.scss"],
-            "assets": [
-              "client/assets",
-              "client/favicon.ico"
-            ]
+            "assets": ["client/assets", "client/favicon.ico"]
           }
         },
         "lint": {
diff --git a/helix-front/client/app/core/helix.service.ts b/helix-front/client/app/core/helix.service.ts
index 1f46d7d..c9e899e 100644
--- a/helix-front/client/app/core/helix.service.ts
+++ b/helix-front/client/app/core/helix.service.ts
@@ -29,7 +29,7 @@
       .pipe(catchError(this.errorHandler));
   }
 
-  protected post(path: string, data: string): Observable<any> {
+  protected post(path: string, data: any): Observable<any> {
     return this.http
       .post(`${Settings.helixAPI}${this.getHelixKey()}${path}`, data, {
         headers: this.getHeaders(),
diff --git a/helix-front/client/app/resource/shared/resource.service.ts b/helix-front/client/app/resource/shared/resource.service.ts
index 601a62f..5c8385c 100644
--- a/helix-front/client/app/resource/shared/resource.service.ts
+++ b/helix-front/client/app/resource/shared/resource.service.ts
@@ -3,10 +3,9 @@
 
 import * as _ from 'lodash';
 
+import { IdealState } from '../../shared/node-viewer/node-viewer.component';
 import { HelixService } from '../../core/helix.service';
 import { Resource } from './resource.model';
-import { Cluster } from '../../cluster/shared/cluster.model';
-import { Node } from '../../shared/models/node.model';
 
 @Injectable()
 export class ResourceService extends HelixService {
@@ -114,12 +113,11 @@
   public setIdealState(
     clusterName: string,
     resourceName: string,
-    idealState: Node
+    idealState: IdealState
   ) {
     return this.post(
       `/clusters/${clusterName}/resources/${resourceName}/idealState?command=update`,
-      JSON.stringify(idealState)
+      idealState
     );
   }
 }
-
diff --git a/helix-front/client/app/shared/node-viewer/node-viewer.component.html b/helix-front/client/app/shared/node-viewer/node-viewer.component.html
index 9e9b95f..e37776b 100644
--- a/helix-front/client/app/shared/node-viewer/node-viewer.component.html
+++ b/helix-front/client/app/shared/node-viewer/node-viewer.component.html
@@ -34,9 +34,15 @@
   </mat-button-toggle-group>
   <section class="viewer" [ngSwitch]="group.value" fxFlexFill>
     <ngx-json-viewer *ngSwitchCase="'tree'" [json]="obj"></ngx-json-viewer>
-    <ace-editor *ngSwitchCase="'json'" [(text)]="objString" mode="json" theme="chrome"
-      [options]="{useWorker: false}" style="min-height:300px;"
-      #editor>
+    <ace-editor
+      *ngSwitchCase="'json'"
+      [(text)]="objString"
+      mode="json"
+      theme="chrome"
+      [options]="{ useWorker: false }"
+      style="min-height: 300px"
+      #editor
+    >
     </ace-editor>
     <section *ngSwitchCase="'table'">
       <!-- TODO vxu: use mat-simple-table when it's available -->
diff --git a/helix-front/client/app/shared/node-viewer/node-viewer.component.ts b/helix-front/client/app/shared/node-viewer/node-viewer.component.ts
index de98a1c..de4f807 100644
--- a/helix-front/client/app/shared/node-viewer/node-viewer.component.ts
+++ b/helix-front/client/app/shared/node-viewer/node-viewer.component.ts
@@ -25,6 +25,13 @@
 import { InputDialogComponent } from '../dialog/input-dialog/input-dialog.component';
 import { ConfirmDialogComponent } from '../dialog/confirm-dialog/confirm-dialog.component';
 
+export type IdealState = {
+  id: string;
+  simpleFields?: { [key: string]: any };
+  listFields?: { [key: string]: any };
+  mapFields?: { [key: string]: any };
+};
+
 config.set(
   'basePath',
   'https://cdn.jsdelivr.net/npm/ace-builds@1.6.0/src-noconflict/'
@@ -374,10 +381,29 @@
 
     const path = this?.route?.snapshot?.data?.path;
     if (path && path === 'idealState') {
+      const idealState: IdealState = {
+        id: this.resourceName,
+      };
+
+      // format the payload the way that helix-rest expects
+      // before: { simpleFields: [{ name: 'NUM_PARTITIONS', value: 2 }] };
+      // after:  { simpleFields: { NUM_PARTITIONS: 2 } };
+      function appendIdealStateProperty(property: keyof Node) {
+        if (Array.isArray(newNode[property]) && newNode[property].length > 0) {
+          idealState[property] = {} as any;
+          (newNode[property] as any[]).forEach((field) => {
+            idealState[property][field.name] = field.value;
+          });
+        }
+      }
+      Object.keys(newNode).forEach((key) =>
+        appendIdealStateProperty(key as keyof Node)
+      );
+
       const observer = this.resourceService.setIdealState(
         this.clusterName,
         this.resourceName,
-        newNode
+        idealState
       );
 
       if (observer) {
@@ -386,7 +412,10 @@
           () => {
             this.helper.showSnackBar('Ideal State updated!');
           },
-          (error) => this.helper.showError(error),
+          (error) => {
+            this.helper.showError(error);
+            this.isLoading = false;
+          },
           () => (this.isLoading = false)
         );
       }
diff --git a/helix-front/client/app/shared/shared.module.ts b/helix-front/client/app/shared/shared.module.ts
index 9f18ece..11bea9b 100644
--- a/helix-front/client/app/shared/shared.module.ts
+++ b/helix-front/client/app/shared/shared.module.ts
@@ -34,7 +34,7 @@
     FormsModule,
     NgxDatatableModule,
     NgxJsonViewerModule,
-    AceEditorModule
+    AceEditorModule,
   ],
   declarations: [
     InputDialogComponent,
diff --git a/helix-front/server/controllers/helix.ts b/helix-front/server/controllers/helix.ts
index 1203b27..d5d9dbd 100644
--- a/helix-front/server/controllers/helix.ts
+++ b/helix-front/server/controllers/helix.ts
@@ -51,6 +51,8 @@
       request[method](options, (error, response, body) => {
         if (error) {
           res.status(500).send(error);
+        } else if (body?.error) {
+          res.status(500).send(body?.error);
         } else {
           res.status(response.statusCode).send(body);
         }